Skip to content

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:

import transithttp "gitlab.com/phpboyscout/go/transit/http"

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() — trust X-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 with NewClientChain(…).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.