grpc/grpc-go · error

negotiated unknown next_protocol %q

Error message

negotiated unknown next_protocol %q

What it means

NewConnWithMaxFrameSize (record.go:144-148) looks up the negotiated record protocol name in the protocols map to obtain its crypto factory. If the handshake selected a next_protocol that has no registered factory, the secure connection cannot be created. The %q is the unknown protocol name returned by the ALTS handshaker.

Source

Thrown at credentials/alts/internal/conn/record.go:147

	// nextFrame stores the next frame (in protected buffer) info.
	nextFrame []byte
	// overhead is the calculated overhead of each frame.
	overhead  int
	constPool constBufferPool // stored as a field to avoid heap allocations.
}

// NewConn creates a new secure channel instance given the other party role and
// handshaking result.
func NewConn(c net.Conn, side core.Side, recordProtocol string, key []byte, protected []byte) (net.Conn, error) {
	return NewConnWithMaxFrameSize(c, side, recordProtocol, key, protected, 0)
}

// NewConnWithMaxFrameSize creates a new secure channel instance given the
// other party role, handshaking result, and negotiated maximum frame size.
func NewConnWithMaxFrameSize(c net.Conn, side core.Side, recordProtocol string, key []byte, protected []byte, negotiatedMaxFrameSize int) (net.Conn, error) {
	newCrypto := protocols[recordProtocol]
	if newCrypto == nil {
		return nil, fmt.Errorf("negotiated unknown next_protocol %q", recordProtocol)
	}
	crypto, err := newCrypto(side, key)
	if err != nil {
		return nil, fmt.Errorf("protocol %q: %v", recordProtocol, err)
	}
	overhead := MsgLenFieldSize + msgTypeFieldSize + crypto.EncryptionOverhead()

	// Clamp maxRecordLen to be at least altsRecordDefaultLength.
	maxRecordLen := max(altsRecordDefaultLength, negotiatedMaxFrameSize)
	payloadLengthLimit := maxRecordLen - overhead
	// We pre-allocate protected to be of size 32KB during initialization.
	// We increase the size of the buffer by the required amount if it can't
	// hold a complete encrypted record.
	protectedHandle := readBufPool.Get(max(altsReadBufferInitialSize, len(protected)))
	protectedBuf := *protectedHandle
	// Copy additional data from hanshaker service.
	copy(protectedBuf, protected)
	protectedBuf = protectedBuf[:len(protected)]

View on GitHub (pinned to 03255a9237)

Solutions

  1. Update the gRPC/ALTS build so the negotiated protocol's factory is registered.
  2. If custom, register your record protocol via conn.RegisterProtocol before handshaking.
  3. Align client and handshaker-service versions so they negotiate a mutually known protocol.

Example fix

// before: build missing ALTSRP_GCM registration
// after
import _ "google.golang.org/grpc/credentials/alts/internal/conn"  // ensures init() registers protocols
// or upgrade the gRPC module to a version that knows the negotiated protocol
Defensive patterns

Strategy: try-catch

Try / catch

c, err := conn.NewConnWithMaxFrameSize(raw, core.ClientSide, rp, key, protected, 0)
if err != nil {
    if strings.Contains(err.Error(), "negotiated unknown next_protocol") {
        log.Printf("ALTS negotiated an unsupported protocol %q; upgrade gRPC", rp)
    }
    return nil, err
}

Prevention

When it happens

Trigger: The ALTS handshake negotiates a record protocol (e.g. a newer ALTSRP_*) that this build of the conn package didn't register. Arises from version skew between the handshaker service result and the compiled-in crypto protocols.

Common situations: A newer handshaker service advertises a protocol the older client lib doesn't know; a custom/forked build missing an init() registration; mismatched gRPC versions across client and GCP handshaker.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/e8cbb2e9661c7ec4. Report an issue: GitHub.