Skip to content

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 0OS-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

const DefaultMaxGRPCMessageBytes = 1 << 20 // 1 MiB

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

func AuthInterceptor(opts ...GRPCAuthOption) (transitgrpc.Interceptor, error)

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

  1. Bearer and API-key metadata together → rejected as ambiguous.
  2. Bearer alone → verified with the bearer verifier.
  3. API-key metadata alone → verified with the API-key verifier.
  4. mTLS → only when no metadata credential was presented, and peer.FromContext carries credentials.TLSInfo with at least one verified chain.
  5. 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

func DialLocal(settings ServerSettings, tlsPair gtls.Pair, opts ...any) (*grpc.ClientConn, error)

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.