gRPC server reference¶
Everything gitlab.com/phpboyscout/go/transport/grpc exposes: the typed
settings, the option families and how they are dispatched, the built-in limits,
the health service, and the errors each entry point returns.
Constructors and lifecycle functions¶
func NewServer(settings ServerSettings, opts ...any) (*grpc.Server, error)
func Register(id string, controller controls.Controllable, logger *slog.Logger,
settings ServerSettings, opts ...any) (*grpc.Server, error)
func Start(logger *slog.Logger, srv *grpc.Server, settings ServerSettings,
tlsPair gtls.Pair, opts ...ServerOption) controls.StartFunc
func Stop(logger *slog.Logger, srv *grpc.Server) controls.StopFunc
func Status(srv *grpc.Server) controls.StatusFunc
func RegisterHealthService(srv *grpc.Server, controller healthSource) (stop func())
func DialLocal(settings ServerSettings, tlsPair gtls.Pair, opts ...any) (*grpc.ClientConn, error)
func TLSServerCredentials(certFile, keyFile string) (credentials.TransportCredentials, error)
func TLSClientCredentials(caFiles ...string) (credentials.TransportCredentials, error)
Register builds the server, registers the health service, binds the health
poller to the service lifecycle, and calls controller.Register(id, …).
NewServer returns a bare *grpc.Server with the message-size defaults applied
and, optionally, reflection registered.
A registered server is single-use
Do not give this service a controls.RestartPolicy. The server is built
once and captured by the start, stop and status functions; a restart rebuilds
the listener and hands it to a server that can no longer serve, and the error
saying so is filtered, so the restart reports success while nothing listens.
See Can a registered server be restarted in-process?
ServerSettings fields¶
type ServerSettings struct {
Host string `mapstructure:"host" yaml:"host" json:"host"`
Port int `mapstructure:"port" yaml:"port" json:"port"`
Reflection bool `mapstructure:"reflection" yaml:"reflection" json:"reflection"`
}
| Field | Default (zero value) | Effect | When it is wrong |
|---|---|---|---|
Host |
"" — binds every interface |
The interface the listener binds. Ignored entirely by DialLocal, which always targets localhost. |
An unownable address fails at start with failed to listen. |
Port |
0 — OS-assigned ephemeral port |
The listen (or, for DialLocal, the dial) port. |
Not range-checked. Only WithPort validates; a bad settings value fails at net.Listen. |
Reflection |
false |
Registers the gRPC server-reflection service, so grpcurl and similar can enumerate the API without a .proto. |
Reflection is always exempt from the auth interceptor — see Which methods skip authentication. |
How the option variadics are dispatched¶
Three entry points take opts ...any and accept different families. An
argument outside the accepted set is an error, never silently discarded:
| Function | Accepts | Rejection message |
|---|---|---|
NewServer |
grpc.ServerOption, ServerOption |
grpc: NewServer received an unsupported option of type <T> (want grpc.ServerOption or ServerOption) |
Register |
ServerOption, RegisterOption, grpc.ServerOption |
grpc: Register received an unsupported option of type <T> (want ServerOption, RegisterOption or grpc.ServerOption) |
DialLocal |
ServerOption, grpc.DialOption |
grpc: DialLocal received an unsupported option of type <T> (want ServerOption or grpc.DialOption) |
NewServer accepts ServerOption values (WithPort, WithHost) and does
nothing with them. The listen address is bound at Start/Register, not at
construction, so they are tolerated for call-site symmetry — one uniform option
set can be passed to both NewServer and Start. If you call NewServer and
then serve the result yourself, WithPort has had no effect.
ServerOption and RegisterOption¶
ServerOption |
Default | Effect |
|---|---|---|
WithPort(int) |
ServerSettings.Port |
Overrides the settings port. Validated: outside 0–65535 yields grpc: invalid port <n> (must be 0-65535). |
WithHost(string) |
ServerSettings.Host |
Overrides the bind interface. No effect on DialLocal. |
WithBindAddress(string) |
— | Exact alias of WithHost. |
WithServerTLSConfig(*tls.Config) |
gtls.DefaultConfig() |
Base config for the managed TLS listener. No effect when the TLS pair is disabled, or on the grpc.Creds path. |
RegisterOption |
Default | Effect |
|---|---|---|
WithTLSPair(gtls.Pair) |
zero pair (Enabled: false) |
Serves TLS from certificate files. A disabled pair means plaintext gRPC. |
WithInterceptors(transitgrpc.InterceptorChain) |
none | Prepended before any interceptors supplied as raw grpc.ServerOption values. |
When an invalid port is reported¶
Not at construction. Start and Register both resolve the port eagerly but
hand the error to the returned StartFunc, and neither Start nor Register
reports it: an out-of-range WithPort surfaces when the supervisor runs the
start function. NewServer never resolves a port at all, so it cannot report
one. Only DialLocal returns the error directly.
The HTTP package differs here — transporthttp.NewServer and
transporthttp.Register both return an invalid-port error immediately.
Message-size limits¶
Every server built through this package — NewServer and Register alike —
gets grpc.MaxRecvMsgSize(DefaultMaxGRPCMessageBytes) and
grpc.MaxSendMsgSize(DefaultMaxGRPCMessageBytes) applied before your
options. gRPC honours the last option wins, so passing your own
grpc.MaxRecvMsgSize(...) raises or lowers the limit.
There is no WithMaxMessageBytes in this package; the raw gRPC option is the
supported route, and it is why the option variadic accepts grpc.ServerOption
at all.
Exceeding the limit is gRPC's own behaviour: codes.ResourceExhausted with a
grpc: received message larger than max message.
The health service: names, statuses and refresh interval¶
newHealthService registers the standard grpc.health.v1.Health service and
sets three serving statuses from the controller:
| Health service name | Sourced from | SERVING when |
|---|---|---|
"" (the default, empty name) |
controller.Status() |
OverallHealthy |
"liveness" |
controller.Liveness() |
OverallHealthy |
"readiness" |
controller.Readiness() |
OverallHealthy |
Anything else is NOT_SERVING. The statuses are set once immediately at
registration, then refreshed by a background poller every 10 seconds. There
is no option to change that interval, and no way to push an update on demand —
a health transition is visible over gRPC within one interval, not instantly.
Who owns the health poller goroutine¶
| Path | Poller lifetime | Your obligation |
|---|---|---|
Register |
Started when the service starts, stopped when it stops. A server that is registered but never started spawns no goroutine. | none |
RegisterHealthService |
Started immediately, bound to controller.GetContext(). |
Call the returned stop() when the server is done. |
RegisterHealthService takes a controller that satisfies both
controls.HealthReporter and GetContext() context.Context. A value typed
as a bare controls.HealthReporter does not satisfy it; pass the
*controls.Controller (or anything implementing controls.Controllable).
Since controls v0.2.0 the controller's context is severed from the caller's
and is cancelled only by the controller's own shutdown. Cancelling your
context no longer reaps the poller, which is why the stop function exists.
AuthInterceptor options¶
Returns a transitgrpc.Interceptor carrying both a unary and a stream
interceptor.
| Option | Effect |
|---|---|
WithGRPCBearerVerifier(authn.Verifier) |
Reads Bearer <token> from the authorization metadata key. |
WithGRPCAPIKeyMetadata(key string, authn.Verifier) |
Reads the named metadata key. The key is lower-cased for you, matching gRPC metadata semantics. |
WithGRPCMTLSVerifier(authn.CertVerifier) |
Authenticates from the peer's verified certificate chains. |
WithGRPCAuthorize(authn.AuthorizeFunc) |
Predicate run after verification; false yields codes.PermissionDenied. |
WithGRPCAuthLogger(*slog.Logger) |
Logger for redacted failure logging. Default: a discard logger. |
WithGRPCMethodSkipper(func(fullMethod string) bool) |
Adds to the always-skipped set; it cannot remove from it. |
With no verifier, construction fails with
grpc: AuthInterceptor requires at least one verifier (fail-closed).
There is no cookie verifier on the gRPC side — the HTTP WithCookieVerifier
has no counterpart, because gRPC has no ambient-credential problem to solve.
Which methods skip authentication¶
Two prefixes are skipped unconditionally, before your skipper is consulted:
/grpc.health.v1.Health/— so Kubernetes gRPC probes keep working./grpc.reflection.v1.and/grpc.reflection.v1alpha.— so reflection-based tooling keeps working.
WithGRPCMethodSkipper adds method names to that set. It cannot take reflection
back out. If you enable Reflection: true on a server behind this interceptor,
an unauthenticated caller can enumerate every service, method and message type
you expose. Leave Reflection off in production, or accept that the API shape
is public.
Which credential wins, and what a failed RPC sees¶
- Bearer and API-key metadata together → rejected as ambiguous.
- Bearer alone → verified with the bearer verifier.
- API-key metadata alone → verified with the API-key verifier.
- mTLS → only when no metadata credential was presented, and
peer.FromContextcarriescredentials.TLSInfowith at least one verified chain. - Nothing presented → unauthenticated.
| Outcome | Status code | Message |
|---|---|---|
| Authentication failed | codes.Unauthenticated |
unauthenticated |
| Authorization denied | codes.PermissionDenied |
permission denied |
The cause is never returned to the caller; it is logged once at WARN with the
credential passed through redact.Error. IdentityFromContext(ctx) reads the
verified identity downstream, sharing its context key with the HTTP middleware,
and stream handlers see the authenticated context because the interceptor wraps
the grpc.ServerStream.
TLS credential helpers¶
| Function | Returns | Notes |
|---|---|---|
TLSServerCredentials(certFile, keyFile) |
Server credentials.TransportCredentials from the hardened config with that key pair loaded. |
For passing to grpc.NewServer via grpc.Creds() instead of the managed Start/Register TLS path — not alongside it. credentials.NewTLS advertises h2 itself. |
TLSClientCredentials(caFiles...) |
Client credentials trusting the given CA/certificate files. | With no files it trusts the system roots. The client-side mirror, e.g. for a gateway dialling a privately-signed gRPC server. |
Combining an enabled WithTLSPair with your own grpc.Creds(...) produces a
server no ordinary client can reach: the listener is already TLS-wrapped, and
your credentials then attempt a second handshake inside that session. Clients
fail with error reading server preface: EOF. Pick one path — the managed pair,
or grpc.Creds on a plaintext listener.
DialLocal¶
Dials localhost:<port>, where the port comes from WithPort if supplied and
settings.Port otherwise. Host and WithHost are ignored — the target is
always localhost; the options are accepted so one settings value can drive both
the server and the in-process client. Transport security is derived from
tlsPair; extra grpc.DialOption values are passed through, and an explicit
transport-credentials option overrides the derived one.
The endpoint assembly and credential selection live in
go/grpcclient; this is a thin adapter
onto grpcclient.Dial.
Error strings this package returns¶
| Error | Raised by | Cause |
|---|---|---|
grpc: invalid port <n> (must be 0-65535) |
DialLocal directly; otherwise the StartFunc built by Start or Register |
WithPort outside the valid range. |
grpc: NewServer received an unsupported option of type <T> (want grpc.ServerOption or ServerOption) |
NewServer |
Wrong option family — e.g. a grpc.DialOption. |
grpc: Register received an unsupported option of type <T> (want ServerOption, RegisterOption or grpc.ServerOption) |
Register |
Wrong option family. |
grpc: DialLocal received an unsupported option of type <T> (want ServerOption or grpc.DialOption) |
DialLocal |
Wrong option family — e.g. a grpc.ServerOption. |
grpc: AuthInterceptor requires at least one verifier (fail-closed) |
AuthInterceptor |
No verifier configured. |
configuring gRPC TLS: … |
the start function | The pair's certificate or key could not be loaded, or its ClientAuth/ClientCAs policy is invalid. |
failed to listen: … |
the start function | The address is in use, out of range, or not ownable. |
grpc: expected a TLS connection on the managed TLS listener, got <T> |
the handshake path | Internal invariant — a non-TLS connection reached the TLS listener credentials. |
Shutdown behaviour¶
Stop calls GracefulStop in a goroutine so in-flight RPCs finish. If the
shutdown context expires first it logs
gRPC graceful stop timed out, forcing stop and calls Stop(). The context
also covers the case where Serve was never called, which would otherwise make
GracefulStop block forever.
Status on the Register path reports the serve goroutine's exit error, so a
server whose serve loop has died stops reporting healthy. The exported
Status(srv) for a hand-wired server only reports an error when srv is nil.