middleware package
import "github.com/okian/servo/v3/middleware"
Who this is for: anyone attaching the commodity middleware instead of writing it.
Everything here is an ordinary graph node with the standard
Middleware(next http.Handler) http.Handler method, attached in the spec with
servo.Use[T](selector...) like middleware you write yourself — same wrap-order rules,
same group/route selectors, same Override story in tests. The package is standard library only.
Two shapes:
- Zero-config — the constructor takes nothing, so resolution finds it as-is and the
Useline is the entire setup:Recover,AccessLog,RequestID,Gzip. - Configured — the constructor takes a
*XxxConfigthat your own provider supplies, the same pattern as*servo.HTTPConfig. The config is a real Go struct built in your code, so fields likeRateLimitConfig.KeyFuncare ordinary functions.
// spec
servo.Build(
servo.HTTP(
servo.Use[*middleware.Recover](), // zero-config
servo.Use[*middleware.CORS](servo.Group("default")), // reads *middleware.CORSConfig
servo.Use[*middleware.RateLimit](), // reads *middleware.RateLimitConfig
),
)
// your module
func NewCORSConfig() *middleware.CORSConfig {
return &middleware.CORSConfig{AllowedOrigins: []string{"https://app.example.com"}}
}
Recover
Panic → 500 with the canonical body only; the panic value and stack go to slog. Re-panics
http.ErrAbortHandler, which is net/http’s own “abort this connection” sentinel.
CORS — CORSConfig
| Field | Meaning |
|---|---|
AllowedOrigins |
exact origins, or the single element "*"; required |
AllowedMethods |
defaults to every method servo routes |
AllowedHeaders |
defaults to echoing the preflight’s request headers |
ExposedHeaders |
stamped on actual responses |
AllowCredentials |
with "*" the concrete origin is echoed — * is invalid alongside credentials |
MaxAge |
preflight cache lifetime; zero omits the header |
Preflights (OPTIONS + Access-Control-Request-Method) are answered with 204 and never reach the
handler. A disallowed origin gets no CORS headers at all — the browser enforces the block — and
Vary: Origin is always set.
RateLimit — RateLimitConfig
A token bucket per key: RPS refills, Burst caps, exhausted keys get 429 with Retry-After.
KeyFunc decides what a key is — nil means the client IP (port stripped). The buckets are
in-process state, swept when idle; two replicas mean two buckets per key.
BodyLimit — BodyLimitConfig
A declared Content-Length over MaxBytes is refused with 413 before the handler runs; chunked
bodies read through http.MaxBytesReader, so the cap holds on the read path too. Complements
HTTPConfig.MaxBodyBytes, which guards only routes that decode bodies (and answers 413 there as
well).
AccessLog
One slog.Info line per request — method, path, status, bytes, duration, remote — through
slog.Default() at call time, so it follows whatever logger main installs.
Timeout — TimeoutConfig
http.TimeoutHandler: a handler exceeding Limit is cut off with 503 and Body (a JSON error by
default), and its request context is cancelled.
RequestID
Reads X-Request-Id when it is at most 128 bytes of printable ASCII and generates one otherwise,
sets it on the response, and plants it in the context;
middleware.RequestIDFromContext(ctx) reads it back in handlers and extractors.
Gzip
Compresses when the client accepts gzip. The decision is made at WriteHeader time: responses
already carrying a Content-Encoding, with inherently compressed content types (image/, video/,
audio/, zip/gzip/zstd), or with a body-less status (1xx, 204, 304), pass through untouched. Sets
Vary: Accept-Encoding.