Compose HTTP middleware¶
transit's http package has two toolboxes: server middleware that wraps an
http.Handler, and client middleware that wraps an http.RoundTripper. This guide
lists what each provides and how to assemble them.
Throughout, the package is imported aliased:
Server middleware¶
Server middleware has type Middleware (func(http.Handler) http.Handler). Build a
chain with NewChain and apply it with Then (or ThenFunc for an
http.HandlerFunc). The chain runs outermost-first.
| Constructor | Purpose |
|---|---|
LoggingMiddleware(log, opts…) |
One structured record per request; safe client-IP extraction. |
OTelMiddleware(server, opts…) |
An OpenTelemetry server span per request. |
RateLimitMiddleware(log, cfg) |
Token-bucket admission control, optionally keyed per client. |
chain := transithttp.NewChain(
transithttp.LoggingMiddleware(log),
transithttp.OTelMiddleware("orders"),
transithttp.RateLimitMiddleware(log, transithttp.DefaultRateLimitConfig()),
)
srv := &http.Server{Handler: chain.Then(mux)}
Tuning the logger¶
LoggingMiddleware takes functional options:
WithLogLevel(slog.Level)— the level for successful requests (5xx always logs at error).WithPathFilter("/healthz", …)— paths to skip (health probes).WithFormat(FormatJSON | FormatCommon | FormatCombined)— structured or Apache-style access logs.WithHeaderFields("x-request-id", …)— copy request headers into the record; known-sensitive headers (Authorization,Cookie, …) are redacted even if you name them.WithTrustedProxy()— trustX-Forwarded-For/X-Real-IP. Off by default, so spoofed proxy headers cannot forge the logged client IP.WithoutLatency(),WithoutUserAgent()— drop those fields.
Rate limiting¶
DefaultRateLimitConfig() is a single global bucket (50 rps, burst 100). A
RateLimitConfig also supports per-key limiting bounded by MaxTrackedKeys so the
key table cannot grow without limit. To layer config sources, merge explicit overrides
onto a base with MergeRateLimitConfig.
Client middleware¶
The client half is composed of http.RoundTripper decorators. Two shapes exist:
NewRetryTransport(next, cfg)— wraps a transport with retry directly.ClientMiddleware(func(http.RoundTripper) http.RoundTripper), assembled withNewClientChain(…).Then(transport).
| Constructor | Kind | Purpose |
|---|---|---|
NewRetryTransport(next, cfg) |
transport | Exponential-backoff retry with full jitter. |
WithCircuitBreaker(log, cfg) |
ClientMiddleware |
Fail fast while a downstream is unhealthy. |
WithBearerToken(token) |
ClientMiddleware |
Authorization: Bearer, pinned to the first host. |
WithBasicAuth(user, pass) |
ClientMiddleware |
Authorization: Basic, host-pinned. |
WithRateLimit(rps) |
ClientMiddleware |
Throttle outbound requests. |
WithRequestLogging(log) |
ClientMiddleware |
Debug-log each outbound request. |
Assembling the client stack¶
Order is deliberate. Retry should sit closest to the raw transport, and the circuit breaker outside retry, so one retry-exhausted logical call counts as a single breaker failure rather than one per attempt:
retrying := transithttp.NewRetryTransport(http.DefaultTransport, transithttp.DefaultRetryConfig())
chain := transithttp.NewClientChain(
transithttp.WithCircuitBreaker(log, transithttp.DefaultCircuitBreakerConfig()),
transithttp.WithBearerToken(token),
transithttp.WithRequestLogging(log),
)
client := &http.Client{Transport: chain.Then(retrying)}
Then applies the chain so the first middleware is the outermost wrapper: the breaker
sees the final post-retry verdict, and the bearer token is attached once per logical
call. The middleware model covers the ordering in
full.
Credential safety
WithBearerToken and WithBasicAuth pin the credential to the first host the
client addresses. Because the middleware is a RoundTripper it also runs on every
redirect hop, where net/http's cross-host Authorization stripping does not apply —
host-pinning stops a redirect to another host from capturing the credential.