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 aboutMove out when the work becomes
request IDs, tracing hooks, access logging, and HTTP metricsbroader observability adapter and export wiring in x/observability
auth and security-header HTTP adapters on top of security/* primitivestenant policy and tenant resolution in x/tenant
thin rate-limit adapters over stable abuse-guard primitivesfeature catalogs of limiter implementations or capability-specific quota behavior
explicit constructor-based HTTP wrappersAPI 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().

PackageConstructorWhat it does
middleware/requestidrequestid.Middleware()Generates or passes through a unique request ID; stores it in context
middleware/recoveryrecovery.Middleware(recovery.Config{Logger: logger})Catches panics, logs sanitized panic metadata, returns 500
middleware/accesslogaccesslog.Middleware(accesslog.Config{Logger: logger})Logs method, path, status, and duration for every request
middleware/authauthmw.Authenticate(authenticator)Constructs auth middleware with startup validation; returns 401 on authentication failure
middleware/securitysecmw.Middleware(secmw.Config{Policy: policy})Sets Content-Security-Policy, X-Frame-Options, HSTS, and related headers
middleware/corscors.Middleware(opts)Handles preflight and cross-origin request headers
middleware/timeouttimeout.Middleware(cfg)Cancels context and returns 504 if handler exceeds the deadline
middleware/ratelimitratelimit.AbuseGuard(cfg)Token-bucket rate limiter; returns 429 with Retry-After
middleware/bodylimitbodylimit.BodyLimit(maxBytes, logger)Rejects requests with body larger than limit with 413
middleware/compressioncompression.Middleware(cfg)Gzip-compresses responses for clients that accept it
middleware/httpmetricshttpmetrics.Middleware(observer)Records HTTP request count, duration, and status distribution
middleware/tracingtracing.Middleware(tracer)Injects or propagates distributed trace context
middleware/concurrencylimitconcurrencylimit.Middleware(max, queue, timeout)Limits concurrent in-flight requests; queues up to queue depth
middleware/debugdebug.Middleware(cfg)Captures response errors for dev-mode inspection
middleware/coalescecoalesce.New(cfg).Middleware()Deduplicates identical in-flight requests (request coalescing)
middleware/conformanceconformance.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.