grpc/grpc-go · error

ClientHandshake() is not supported for server credentials

Error message

ClientHandshake() is not supported for server credentials

What it means

Returned by credsImpl.ClientHandshake (credentials/xds/xds.go:96) when c.isClient is false. The xDS credsImpl is shared for both client and server and carries an isClient flag (set in NewClientCredentials/NewServerCredentials); calling the client-side handshake method on a server-credentials instance is a misuse, so it is rejected explicitly rather than attempting a wrong-role TLS handshake.

Source

Thrown at credentials/xds/xds.go:96

// credsImpl is an implementation of the credentials.TransportCredentials
// interface which uses xDS APIs to fetch its security configuration.
type credsImpl struct {
	isClient bool
	fallback credentials.TransportCredentials
}

// ClientHandshake performs the TLS handshake on the client-side.
//
// It looks for the presence of a HandshakeInfo value in the passed in context
// (added using a call to NewContextWithHandshakeInfo()), and retrieves identity
// and root certificates from there. It also retrieves a list of acceptable SANs
// and uses a custom verification function to validate the certificate presented
// by the peer. It uses fallback credentials if no HandshakeInfo is present in
// the passed in context.
func (c *credsImpl) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
	if !c.isClient {
		return nil, nil, errors.New("ClientHandshake() is not supported for server credentials")
	}

	// The clusterimpl balancer constructs a new HandshakeInfo using a call to
	// NewHandshakeInfo(), and then adds it to the attributes field of the
	// resolver.Address when handling calls to NewSubConn(). The transport layer
	// takes care of shipping these attributes in the context to this handshake
	// function. We first read the credentials.ClientHandshakeInfo type from the
	// context, which contains the attributes added by the clusterimpl balancer.
	// We then read the HandshakeInfo from the attributes to get to the actual
	// data that we need here for the handshake.
	chi := credentials.ClientHandshakeInfoFromContext(ctx)
	// If there are no attributes in the received context or the attributes does
	// not contain a HandshakeInfo, it could either mean that the user did not
	// specify an `xds` scheme in their dial target or that the xDS server did
	// not provide any security configuration. In both of these cases, we use
	// the fallback credentials specified by the user.
	if chi.Attributes == nil {
		return c.fallback.ClientHandshake(ctx, authority, rawConn)

View on GitHub (pinned to 03255a9237)

Solutions

  1. Use xds.NewClientCredentials for client-side channels and xds.NewServerCredentials for servers — never interchange them.
  2. Audit the code path: ensure the creds passed to grpc.Dial came from NewClientCredentials and those passed to grpc.NewServer came from NewServerCredentials.
  3. Name credential variables by role (clientXdsCreds vs serverXdsCreds) to make the mix-up visually obvious.

Example fix

// before
creds, _ := xds.NewServerCredentials(xds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(creds)) // ClientHandshake fails

// after
creds, _ := xds.NewClientCredentials(xds.ClientOptions{FallbackCreds: insecure.NewCredentials()})
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(creds))
Defensive patterns

Strategy: type-guard

Type guard

// Ensure the creds came from the client constructor before dialing.
// credsImpl is unexported, so guard by construction: tag your own wrapper.
type clientXDS struct{ credentials.TransportCredentials }

func mustClientXDS(c credentials.TransportCredentials, isClient bool) clientXDS {
    if !isClient { panic("server creds passed to client") }
    return clientXDS{c}
}

Try / catch

// ClientHandshake errors are surfaced on the first RPC; detect at startup by
// performing a no-op connectivity check, or simply keep client/server creds
// in separately-named variables to prevent the mix-up.

Prevention

When it happens

Trigger: Creating credentials with xds.NewServerCredentials(...) and then handing them to a gRPC client via grpc.WithTransportCredentials (or otherwise invoking ClientHandshake on server creds). The handshake is attempted on the first outbound RPC.

Common situations: Swapping client and server credential construction in a full-duplex service; sharing one creds variable across both roles; copy-paste between client and server setup code.

Understand the failure class

Related errors


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