Skip to content

Dial options: what Dial sets and what you can override

Dial sets exactly one dial option of its own and passes everything else through. This page states what that option is, how it interacts with yours, and which options are worth setting.

The only option Dial sets is the transport credentials

dialOpts := append([]grpc.DialOption{grpc.WithTransportCredentials(creds)}, opts...)

conn, err := grpc.NewClient(endpoint, dialOpts...)

creds comes from Target.TLS. Nothing else is set: no keepalive, no message-size limits, no service config, no default call options, no resolver, no balancer. Whatever the gRPC SDK's own defaults are, that is what you get.

That means you never pass grpc.WithTransportCredentials yourself. Set TLS.Enabled and TLS.Cert on the Target instead — see Target fields.

Your options are applied after Dial's, so yours win

gRPC applies dial options in order and later ones overwrite earlier ones — grpc.WithTransportCredentials is a plain assignment to the internal options struct. Dial places its credentials first, so a caller-supplied grpc.WithTransportCredentials replaces them.

This is a real hole, not a theoretical one. The following dials in cleartext even though the target asked for TLS:

conn, _ := grpcclient.Dial(
    grpcclient.Target{Port: port, TLS: gtls.Pair{Enabled: true}},
    grpc.WithTransportCredentials(insecure.NewCredentials()), // wins
)

The connection reaches READY against a plaintext server. Dial returns no error and logs nothing.

Ordering credentials first is the wrong way round for a security default, and this documentation previously claimed the opposite — that "a caller's option can never silently downgrade the target's transport security". It can. Until the ordering changes, the rule to work to is: Target.TLS is the intent, and any transport-credential option you add overrides it. Do not accept dial options from configuration or from a caller you do not control.

Options that Dial rejects at dial time

Two option combinations make grpc.NewClient fail, so Dial returns an error rather than a connection:

// grpc: credentials.Bundle may not be used with individual TransportCredentials
grpcclient.Dial(target, grpc.WithCredentialsBundle(b))

// grpc: the credentials require transport level security
// (use grpc.WithTransportCredentials() to set)
grpcclient.Dial(target /* TLS disabled */, grpc.WithPerRPCCredentials(tokenSource))

grpc.WithCredentialsBundle can never be combined with Dial, because Dial always sets individual transport credentials. grpc.WithPerRPCCredentials works only when Target.TLS.Enabled is true, because token-bearing credentials refuse to travel over an insecure connection.

Options worth passing

Anything of type grpc.DialOption is accepted. These are the ones that come up:

Option Why
transitgrpc.OTelClientHandler() OpenTelemetry client instrumentation, from go/transit
grpc.WithChainUnaryInterceptor(transitgrpc.CircuitBreakerInterceptor(log, cfg)) Fail-fast circuit breaking on unary calls
grpc.WithChainStreamInterceptor(transitgrpc.CircuitBreakerStreamInterceptor(log, cfg)) The same for streams
grpc.WithKeepaliveParams(...) Detect a dead peer; Dial sets no keepalive
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(n)) Raise the 4 MiB receive limit the SDK defaults to
grpc.WithDefaultServiceConfig(json) Retry policy and load balancing; Dial configures neither
grpc.WithAuthority(name) Verify the server certificate against name rather than Target.Host
grpc.WithUserAgent(s) Identify your client in the server's logs

Dial a service with middleware shows the interceptors composed into one call.

Verifying a certificate under a different name

Dial cannot set ServerName, so the certificate is checked against Target.Host. To dial an address and verify a different name — a load-balancer VIP, or a sidecar on loopback fronting a named service — pass the authority yourself:

conn, err := grpcclient.Dial(
    grpcclient.Target{Port: 8443, TLS: gtls.Pair{Enabled: true, Cert: caPath}},
    grpc.WithAuthority("svc.internal"),
)

That dials localhost:8443 and verifies the certificate against svc.internal. Without it, the handshake fails with x509: certificate is valid for svc.internal, not localhost.