Skip to content

Middleware Model

Plumego middleware follows the standard Go pattern: func(http.Handler) http.Handler. Every middleware in the system wraps the next handler in the chain. There is no magic — the stack is built at app.Prepare() time from the exact order you called app.Use.

When a request arrives, middleware runs outermost-first on the way in and innermost-first on the way out:

Request → requestid → recovery → accesslog → [handler]
Response ← requestid ← recovery ← accesslog ← [handler]

This means:

  • requestid runs first — every subsequent middleware and handler sees the request ID in context.
  • recovery catches panics from anything that runs after it — put it as early as possible.
  • accesslog logs the response status and duration after the handler returns.
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})
if err != nil {
return err
}
accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(requestid.Middleware(), recoveryMw, accesslogMw)

Use accepts one or more middleware. The call order is the execution order. Call Use before app.Prepare() — after Prepare the chain is frozen.

Stable middleware packages must stay transport-only. That means:

  • They may read and write HTTP headers, status codes, and request context.
  • They must not own tenant policy, quota enforcement, or capability-specific business logic.
  • Tenant resolution goes in x/tenant/transport. Protocol negotiation goes in x/rest/versioning or x/gateway.

If your middleware needs to know the current tenant, read a quota, or make a product decision, it belongs outside the stable middleware layer.

All packages are under github.com/spcent/plumego/middleware/.

PackageConstructorTypical usage
requestidrequestid.Middleware()First in chain. Generates or passes through X-Request-ID; stores ID in context via contract.RequestIDFromContext.
recoveryrecovery.Middleware(recovery.Config{Logger: logger})Second in chain. Catches panics, logs sanitized panic metadata, returns 500.
accesslogaccesslog.Middleware(accesslog.Config{Logger: logger})Third in chain. Logs method, path, status code, and latency for every request.
authauthmw.Authenticate(authenticator)Constructs auth middleware with startup validation. Runtime failures return 401 or 403.
securitysecmw.Middleware(secmw.Config{Policy: &policy})Sets hardening headers: CSP, X-Frame-Options, HSTS, X-Content-Type-Options.
corscors.Middleware(opts)Handles preflight and adds CORS response headers.
timeouttimeout.Middleware(cfg)Cancels the request context after the configured duration; returns 504.
ratelimitratelimit.AbuseGuard(cfg)Token-bucket rate limiter per IP. Returns 429 with Retry-After.
bodylimitbodylimit.BodyLimit(maxBytes, logger)Rejects requests whose body exceeds the limit with 413.
compressioncompression.Middleware(cfg)Gzip-compresses responses for clients that accept gzip.
httpmetricshttpmetrics.Middleware(observer)Records request count, status distribution, and latency via a metrics.HTTPObserver.
tracingtracing.Middleware(tracer)Injects or propagates a distributed trace context.
concurrencylimitconcurrencylimit.Middleware(max, queue, timeout)Caps concurrent in-flight requests; queues up to queue depth, rejects with 503 beyond.
debugdebug.Middleware(cfg)Dev-mode error capture. Do not use in production.
coalescecoalesce.New(cfg).Middleware()Request coalescing — deduplicates identical concurrent requests, returning one shared response.
conformanceconformance.Middleware(...)Validates requests and responses against declared API contracts.

Stack order matters. Follow this sequence for a typical API service:

app.Use(requestid.Middleware()) // 1. ID before everything
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(recoveryMw) // 2. panic recovery
accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(accesslogMw) // 3. log all requests
// Optional, order within the block matters less
app.Use(cors.Middleware(corsOpts)) // 4. CORS headers before any auth check
policy := headers.DefaultPolicy()
securityMw, err := secmw.Middleware(secmw.Config{Policy: &policy})
if err != nil {
return err
}
app.Use(securityMw) // 5. security headers
app.Use(rl.AbuseGuard(rl.DefaultAbuseGuardConfig())) // 6. rate limit before auth
app.Use(to.Middleware(to.Config{Timeout: 10 * time.Second})) // 7. timeout
authMw, err := authmw.Authenticate(authenticator)
if err != nil {
return err
}
app.Use(authMw) // 8. auth last in the default stack

Any func(http.Handler) http.Handler is valid middleware. Use the middleware.Middleware type alias to register it with app.Use:

import mw "github.com/spcent/plumego/middleware"
func TenantHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := r.Header.Get("X-Tenant-ID")
if tenantID == "" {
contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("header", "X-Tenant-ID").
Build())
return
}
ctx := context.WithValue(r.Context(), tenantKey{}, tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
app.Use(mw.Middleware(TenantHeader))

For business-level middleware (tenant policy, quota, session), do not place the logic in the middleware package. Keep it in your application code or an owning x/* family.