grpc/grpc-go · error

negotiated unknown next_protocol

Error message

negotiated unknown next_protocol %q

What it means

Returned by conn.NewConnWithMaxFrameSize when the recordProtocol string negotiated by the ALTS handshake is not found in the 'protocols' map (i.e. no ALTSRecordFunc is registered for it). The only protocol registered by the handshaker is 'ALTSRP_GCM_AES128_REKEY'; any other name triggers this.

Solutions

  1. Upgrade grpc-go to a version that registers the negotiated record protocol.
  2. If running a custom handshaker service, ensure it only negotiates 'ALTSRP_GCM_AES128_REKEY' for clients/servers on this grpc-go version.
  3. If you intentionally added a new protocol, make sure its ALTSRecordFunc is registered via conn.RegisterProtocol before the handshake runs (e.g. via a blank import).
Defensive patterns

Strategy: try-catch

Try / catch

// Treat as a fatal ALTS handshake failure; surface to the caller and retry the RPC.
if _, _, err := chs.ClientHandshake(ctx); err != nil {
    if strings.Contains(err.Error(), "negotiated unknown next_protocol") {
        return fmt.Errorf("ALTS peer negotiated an unsupported record protocol; upgrade grpc-go: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: The ALTS handshaker service returns a HandshakerResult.RecordProtocol whose value is not 'ALTSRP_GCM_AES128_REKEY', and the secure connection cannot be constructed. Reached via doHandshake -> conn.NewConnWithMaxFrameSize during a client or server ALTS handshake on GCP.

Common situations: A version mismatch between the grpc-go client/server and the GCP metadata-server handshaker service that negotiates a newer record protocol (e.g. a future AES-256 variant) not yet supported by this grpc-go build; a test mock handshaker returning a made-up protocol name; a custom ALTSRecordFunc registration that was skipped (e.g. blank import missing).

Related errors


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

Appendix: 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 0c51461d27)