What transport does not do¶
A list of things this module deliberately does not support, with the reason, so you can stop looking. Several of them are answered by a sibling module; the rest are answered by "not yet" or "not here".
If a question of yours ends up here, that is the answer — the absence is intentional, not an oversight waiting for a bug report.
Can it read my configuration file or environment variables?¶
No. transport reads no config file, no environment variable and no
command-line flag. Every setting is a Go struct field or a functional option
passed in code.
The mapstructure/yaml/json tags on ServerSettings are there so a
consumer can decode its own configuration into the struct. Nothing in this
module performs that decode. That is the whole point of the
framework seam: the config-reading
*FromContainable adapters live in go-tool-base, and this module stays free of
Viper, Cobra and the rest — a boundary a dependency-footprint test enforces.
Can it reload settings without a restart?¶
No. The ServerSettingsSource interfaces in transport/http and
transport/grpc (and SettingsSource in transport/gateway) are declared for
consumers to implement and are not consumed anywhere in this module. Nothing
here watches them, polls them, or rebuilds a server when Version() changes.
A port, a bind address, a timeout or a body limit is fixed for the life of the
server you constructed. Changing one means constructing a new server and cycling
the service through the controls supervisor.
Can it reload a renewed certificate without a restart?¶
Not through WithTLSPair. The pair's certificate files are read once, at start,
and the loaded certificate is held for the life of the server. A certificate
renewed on disk is not picked up.
There is a route, but it is yours to build: supply a *tls.Config through
WithServerTLSConfig with a GetCertificate callback that returns the current
certificate. The pair merge preserves your hooks, and on the HTTP side a config
carrying GetCertificate is treated as TLS intent on its own, so no pair is
needed at all.
Can it serve cleartext HTTP/2 (h2c)?¶
No. Over TLS the HTTP server negotiates HTTP/2 through ALPN, advertising
h2, http/1.1. Over plain HTTP it speaks HTTP/1.1 only, and there is no option
to enable h2c. A gRPC client cannot talk to the plaintext HTTP server, and a
plaintext-HTTP/2 client cannot either.
The gRPC listener is a separate thing and does advertise h2 — that is what
makes plaintext gRPC work. It is not the same listener as the HTTP one.
Can it listen on a Unix socket, or on more than one address?¶
No, on both counts. Both listeners are created with Listen(ctx, "tcp", …), so
the network is TCP and not configurable. Host takes a single address, so one
server binds one interface — an empty Host binds all of them, but there is no
way to bind two named interfaces and not a third. Two interfaces means two
servers.
Can I change the health endpoint paths?¶
No. Register mounts /healthz, /livez and /readyz on its own mux, ahead
of your handler, and the paths are compiled in. A handler of yours serving
/healthz is shadowed on that path.
If you need different paths, use NewServer and mount the exported
HealthHandler, LivenessHandler and ReadinessHandler wherever you like —
they are ordinary http.HandlerFunc values. You then also own mounting them
outside your middleware chain, which Register was doing for you.
Can I make the gRPC health status update faster than every 10 seconds?¶
No. The background poller refreshes the gRPC health service on a fixed ten-second ticker, and there is no option to change it or to push an update on demand. A health transition is visible to a gRPC probe within one interval.
The HTTP health endpoints have no such delay — they call the controller on every request.
Can I require authentication on gRPC reflection?¶
No. The auth interceptor skips /grpc.health.v1.Health/,
/grpc.reflection.v1. and /grpc.reflection.v1alpha. unconditionally, before
your WithGRPCMethodSkipper is consulted, so a skipper can add exemptions but
never remove these.
Health is skipped so Kubernetes gRPC probes work without a credential.
Reflection rides along with it. The consequence is worth stating plainly: with
Reflection: true, an unauthenticated caller can enumerate every service,
method and message type on the server. The only control is Reflection: false.
Can I set a different timeout or body limit per route?¶
Not at registration, but yes per request. ReadTimeout, WriteTimeout,
IdleTimeout and the request-body cap are all server-level settings that apply
to every route, and there is no way to declare "this path is different" when you
build the server. What each one has instead is an escape the handler takes
against the request it is serving.
For the timeouts, a per-request escape exists and is the supported answer: clear
the deadline for one handler with
http.NewResponseController(w).SetWriteDeadline(time.Time{}). Setting a
server-wide WithWriteTimeout(0) to accommodate one streaming route removes the
server's only bound on a client that opens a response and stops reading — see
Stream a response.
For the body cap the escape is WithBodyLimit, which sets the limit for one
request in either direction:
func upload(w http.ResponseWriter, r *http.Request) {
r, err := transporthttp.WithBodyLimit(w, r, 10<<20)
if err != nil {
http.Error(w, "server misconfigured", http.StatusInternalServerError)
return
}
// r.Body now reads up to 10 MiB; every other route keeps the server cap.
}
Use the returned request. The one you passed in is untouched, so discarding the result silently leaves the server-wide cap in force. When loosening that is harmless — the oversized body is rejected and you notice immediately. When tightening it is not: the looser cap stays, and the stricter limit you asked for is simply never applied.
Note what does not work, because it looks like it should. Wrapping an
individual handler in MaxBytesMiddleware can only ever lower the effective
cap. Register mounts its own MaxBytesMiddleware outside your handler, and
http.MaxBytesReader wraps the body, so the outermost — tightest — limit is the
one that bites:
your inner MaxBytesMiddleware |
effective limit |
|---|---|
| smaller than the server cap | your inner limit |
| larger than the server cap | the server cap — your limit does nothing |
WithBodyLimit is the supported answer in both directions; reach for it rather
than for a second MaxBytesMiddleware.
Can I use a TLS pair and my own grpc.Creds together?¶
No — and the failure is not obvious. An enabled pair wraps the gRPC listener in
tls.NewListener and installs internal credentials that surface the listener's
TLS state to peer.FromContext. A caller-supplied grpc.Creds(...) replaces
those credentials but does not unwrap the listener, so your credentials attempt a
second handshake inside the established TLS session. Clients fail with
error reading server preface: EOF.
Pick one: the managed pair (and let the module own transport security), or
grpc.Creds with the pair left disabled.
Can I serve gRPC TLS from an in-memory certificate?¶
Not directly. WithServedCertificate exists on transport/http only. On the
gRPC side, build the *tls.Config yourself with the certificate on it and pass
grpc.Creds(credentials.NewTLS(cfg)), with the pair left disabled.
Does it do CORS, compression, rate limiting, tracing or metrics?¶
No — none of those are here, and that is the module boundary rather than a gap:
| You want | It lives in |
|---|---|
| Request logging, OpenTelemetry, rate limiting, circuit breaking | go/transit, composed via a Chain / InterceptorChain |
Prometheus metrics, a /metrics endpoint, pprof |
go/transport-metrics |
| An OpenAPI document and a docs UI | go/transport-openapi |
| Telemetry setup and exporters | go/observability |
| CORS, compression, anything else per-request | your own http.Handler middleware, wrapped in a transit Chain |
transport owns the server shell and its lifecycle. Anything that wraps a
request or an RPC on the way through is middleware, and middleware is transit's
job.
Does it give me an HTTP or gRPC client?¶
Only DialLocal, and only for the in-process gRPC server a gateway sits in
front of. For anything else use
go/httpclient or
go/grpcclient.
The split is deliberate and is the reason a client-only consumer does not drag
in controls, authn and the grpc-gateway runtime. See
The server/client split.
Does it route, mount sub-paths, or proxy?¶
No. Both NewServer and Register take a single http.Handler, and routing
inside it is entirely yours — http.ServeMux, chi, whatever you like. The only
paths this module claims are the three health endpoints on the Register path.
Does it do authorization, sessions, or an OAuth flow?¶
No. WithAuthorize / WithGRPCAuthorize take a predicate
func(context.Context, *authn.Identity) bool and that is the entire
authorization model: a boolean, evaluated after authentication, that turns into a
403 or a PermissionDenied.
Verifying a credential is go/authn's job; minting one, renewing it, and managing a session are nobody's job in this stack. The cookie verifier reads a cookie you set — it does not set one, expire one, or know what a session is.
Can a registered server be restarted in-process?¶
No, and it fails quietly, so do not give a registered server a
controls.RestartPolicy.
Register builds the server once and captures it in the start, stop and status
functions; only the listener is rebuilt per start. Neither a *grpc.Server after
GracefulStop nor an *http.Server after Shutdown can serve again, so a
restart hands a fresh listener to a dead server. The serve goroutine filters the
error that would have said so (grpc.ErrServerStopped, http.ErrServerClosed),
which leaves the restart reporting success while nothing is listening — and the
gRPC server closes the fresh listener it was handed on its way out.
Nothing in the shipped registration path installs a restart policy, so this
cannot happen by accident today. It becomes reachable the moment you compose the
exported Start, Stop and Status with controls.WithRestartPolicy
yourself. Recover by restarting the process; issue 10 tracks making
the servers restartable.
Does it do zero-downtime restarts or listener handoff?¶
No. There is no socket handoff between processes and no graceful-restart
machinery. Stop drains in-flight requests and then releases the listener; the
window between releasing it and the replacement binding is yours to cover, at
the load balancer or the orchestrator.