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
- Use xds.NewClientCredentials for client-side channels and xds.NewServerCredentials for servers — never interchange them.
- Audit the code path: ensure the creds passed to grpc.Dial came from NewClientCredentials and those passed to grpc.NewServer came from NewServerCredentials.
- 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
- Name variables by role: clientXdsCreds vs serverXdsCreds.
- Construct client and server creds in separate, clearly-labeled functions.
- Never reuse one creds variable for both grpc.Dial and grpc.NewServer.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- ServerHandshake is not supported for client credentials
- serverName for peer validation must be configured as a list
- missing fallback credentials
- token file access error
- empty token_exchange_service_uri in options
AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07).
Data as JSON: /api/errors/0a95b4133d97ce65.
Report an issue: GitHub.