Skip to content

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.