Skip to content

Middleware Primer

Open this page after Stable Roots when the change still belongs to the stable surface, but the real question has narrowed to transport-only HTTP middleware.

middleware owns request/response wrappers, ordering-sensitive transport behavior, and request instrumentation that stays independent from business policy.

app.Use(requestid.Middleware())
app.Use(recovery.Recovery(logger))
app.Use(accesslog.Middleware(logger))
  • you are adding or adjusting func(http.Handler) http.Handler behavior
  • the work is about ordering-sensitive HTTP transport concerns
  • the change instruments requests without owning tenant or product policy
  • the behavior is business validation, tenant resolution, or tenant quota policy
  • the task is API version negotiation or protocol transformation
  • the work is really app construction rather than a transport wrapper

First files to read in the current repository

Section titled “First files to read in the current repository”
  1. middleware/module.yaml
  2. docs/reference/canonical-style-guide.md
  3. the target package under middleware/*
Keep it in middleware when the work is about Move out when the work becomes
request IDs, tracing hooks, access logging, and HTTP metrics broader observability adapter and export wiring in x/observability
auth and security-header HTTP adapters on top of security/* primitives tenant policy and tenant resolution in x/tenant
thin rate-limit adapters over stable abuse-guard primitives feature catalogs of limiter implementations or capability-specific quota behavior
explicit constructor-based HTTP wrappers API version negotiation in x/rest/versioning or protocol adaptation in x/gateway/*
app.Use(mw1, mw2, mw3) // runs in registration order, outermost first

Use must be called before app.Prepare().

Package Constructor What it does
middleware/requestid requestid.Middleware() Generates or passes through a unique request ID; stores it in context
middleware/recovery recovery.Middleware(recovery.Config{Logger: logger}) Catches panics, logs sanitized panic metadata, returns 500
middleware/accesslog accesslog.Middleware(accesslog.Config{Logger: logger}) Logs method, path, status, and duration for every request
middleware/auth authmw.Authenticate(authenticator) Constructs auth middleware with startup validation; returns 401 on authentication failure
middleware/security secmw.Middleware(secmw.Config{Policy: policy}) Sets Content-Security-Policy, X-Frame-Options, HSTS, and related headers
middleware/cors cors.Middleware(opts) Handles preflight and cross-origin request headers
middleware/timeout timeout.Middleware(cfg) Cancels context and returns 504 if handler exceeds the deadline
middleware/ratelimit ratelimit.AbuseGuard(cfg) Token-bucket rate limiter; returns 429 with Retry-After
middleware/bodylimit bodylimit.BodyLimit(maxBytes, logger) Rejects requests with body larger than limit with 413
middleware/compression compression.Middleware(cfg) Gzip-compresses responses for clients that accept it
middleware/httpmetrics httpmetrics.Middleware(observer) Records HTTP request count, duration, and status distribution
middleware/tracing tracing.Middleware(tracer) Injects or propagates distributed trace context
middleware/concurrencylimit concurrencylimit.Middleware(max, queue, timeout) Limits concurrent in-flight requests; queues up to queue depth
middleware/debug debug.Middleware(cfg) Captures response errors for dev-mode inspection
middleware/coalesce coalesce.New(cfg).Middleware() Deduplicates identical in-flight requests (request coalescing)
middleware/conformance conformance.Middleware(...) Validates request/response conformance against declared contracts

This is the canonical ordering from the reference service:

import (
"github.com/spcent/plumego/middleware/requestid"
"github.com/spcent/plumego/middleware/recovery"
"github.com/spcent/plumego/middleware/accesslog"
)
app.Use(requestid.Middleware()) // 1. assign ID first
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(recoveryMw) // 2. recover panics
accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(accesslogMw) // 3. log requests

Add auth, CORS, rate limiting, and timeout as needed for your service:

import (
authmw "github.com/spcent/plumego/middleware/auth"
cors "github.com/spcent/plumego/middleware/cors"
to "github.com/spcent/plumego/middleware/timeout"
)
app.Use(cors.Middleware(cors.CORSOptions{AllowedOrigins: []string{"https://myapp.com"}}))
app.Use(rateLimitMiddleware)
app.Use(to.Middleware(to.Config{Timeout: 10 * time.Second}))
authMw, err := authmw.Authenticate(jwtManager.Authenticator(jwt.TokenTypeAccess))
if err != nil {
return err
}
app.Use(authMw)

Any func(http.Handler) http.Handler works:

func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// pre-processing
next.ServeHTTP(w, r)
// post-processing
})
}
app.Use(middleware.Middleware(MyMiddleware))

middleware is easy to overgrow because many behaviors happen in the request path. The current repository keeps the line narrow on purpose: if the behavior is no longer transport-only, the stable middleware layer is already the wrong place.