Skip to content

What grpcclient does not do

This module is a dial factory: it turns a Target into a *grpc.ClientConn and stops. Almost everything a gRPC client needs beyond that is somewhere else, and a few things are not available anywhere. This page lists both, so you can stop looking.

If a capability is missing here and you can express it as a grpc.DialOption, you can have it — Dial passes options straight through. See Dial options.

Mutual TLS is not supported

Dial never sends a client certificate. It builds credentials with gtls.ClientConfig(...), which sets RootCAs and leaves tls.Config.Certificates empty, so the client has no identity to present.

gtls.Pair has a Key field and Dial ignores it. Nothing warns you. Against a server configured with ClientAuth: require-verify the first RPC fails with:

rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: remote error: tls: certificate required"

The ClientCAs and ClientAuth fields of gtls.Pair are ignored for the same reason: they configure a server's policy on incoming client certificates and mean nothing on a dial. An invalid ClientAuth string, which gtls.ServerConfig would reject outright, is silently accepted here.

If you need mutual TLS today, build the credentials yourself and pass them as an option — accepting that this overrides Target.TLS entirely:

cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
    return err
}

pool, err := gtls.CertPool(caPath)
if err != nil {
    return err
}

cfg := gtls.DefaultConfig()
cfg.Certificates = []tls.Certificate{cert}
cfg.RootCAs = pool

conn, err := grpcclient.Dial(
    grpcclient.Target{Host: "svc.internal", Port: 443}, // TLS left disabled — the option decides
    grpc.WithTransportCredentials(credentials.NewTLS(cfg)),
)

The TLS configuration is fixed and cannot be relaxed

Target.TLS exposes two knobs: on or off, and which CA to trust. Everything else in the handshake comes from go/tls's DefaultConfig and is not reachable through Target:

  • You cannot lower or raise the TLS 1.2 minimum version.
  • You cannot change the cipher suite list or the curve preferences.
  • You cannot set ServerName — the certificate is checked against Target.Host. Use grpc.WithAuthority for that.
  • You cannot set InsecureSkipVerify. There is no "trust anything" mode, deliberately. For a self-signed development server, point TLS.Cert at the server's own certificate file instead — that trusts exactly that certificate and nothing more.

Dial does not validate the Target

There is no validation step and no error for a nonsense target. Port: 0, Port: -1, a hostname with a scheme on it, an unbracketed IPv6 literal — all of them return a *grpc.ClientConn without complaint, and fail on the first RPC. See What Dial never fails on.

If a Target is assembled from user input or configuration, validate it yourself.

Target cannot express a gRPC target URI

Dial builds the endpoint with fmt.Sprintf("%s:%d", host, t.Port). There is no way to reach gRPC's URI-based target syntax through Target, so these are all out of reach:

  • Unix domain sockets — unix:///var/run/svc.sock becomes unix:///var/run/svc.sock:0.
  • An explicit resolver scheme — dns:///, xds:///, a custom registered resolver.
  • Multiple addresses behind one target for client-side load balancing.

If you need any of those, call grpc.NewClient directly and construct the credentials with gtls.ClientConfig yourself. There is nothing else in Dial you would be giving up.

No retries, timeouts, circuit breaking or tracing

Dial sets no service config, no default call options and no interceptors. It gives you the gRPC SDK's own defaults, including the 4 MiB maximum receive message size and no retry policy.

You want Where it comes from
Circuit breaking transitgrpc.CircuitBreakerInterceptor / CircuitBreakerStreamInterceptor
OpenTelemetry traces and metrics transitgrpc.OTelClientHandler()
Retries and backoff grpc.WithDefaultServiceConfig with a retryPolicy
Per-call deadlines A context.Context with a deadline at the call site
Keepalive / dead-peer detection grpc.WithKeepaliveParams
Larger messages grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(n))

grpcclient does not depend on go/transit

The documentation used to say it did. It does not: the module's go.mod requires github.com/cockroachdb/errors, gitlab.com/phpboyscout/go/tls, google.golang.org/grpc and (for tests) github.com/stretchr/testify, and nothing else. The OpenTelemetry packages in the dependency graph arrive via the gRPC SDK, not via transit.

That is the correct arrangement — an interceptor is a choice the consumer makes — but it means go get gitlab.com/phpboyscout/go/grpcclient does not give you transitgrpc.OTelClientHandler. Run go get gitlab.com/phpboyscout/go/transit in your own module as well.

There is no server side, and no lifecycle management

grpcclient dials. It does not serve, does not register health services, and does not integrate with go/controls for startup ordering or graceful shutdown. Closing the connection is yours to do, with conn.Close().

The server half of the same transport stack lives in go-tool-base, and a depfootprint guard test in this repository fails the build if it ever appears in the dependency graph.

No logging, no metrics, no configuration binding

The module never logs and never emits a metric — there is no logger to inject and no place to hook one. Instrumentation is what the transit interceptors are for.

Target is also a plain struct with no config-file or environment-variable binding of its own. It carries no mapstructure or yaml tags at the top level, though the embedded gtls.Pair does. If you want a Target from configuration, unmarshal into your own type and construct the Target from it.

Transport credentials can be overridden by a caller's dial option

Dial places its credentials first in the option list, and gRPC lets a later option overwrite an earlier one. A caller-supplied grpc.WithTransportCredentials therefore wins over Target.TLS, including one that downgrades a TLS target to plaintext. This is a known weakness rather than a design intent, and it is described in full under Your options are applied after Dial's, so yours win.