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 againstTarget.Host. Usegrpc.WithAuthorityfor that. - You cannot set
InsecureSkipVerify. There is no "trust anything" mode, deliberately. For a self-signed development server, pointTLS.Certat 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.sockbecomesunix:///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.