Middleware Model
Middleware Model
Section titled “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.
Execution model
Section titled “Execution model”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:
requestidruns first — every subsequent middleware and handler sees the request ID in context.recoverycatches panics from anything that runs after it — put it as early as possible.accessloglogs the response status and duration after the handler returns.
Registering middleware
Section titled “Registering middleware”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.
Transport-only constraint
Section titled “Transport-only constraint”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 inx/rest/versioningorx/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.
Built-in middleware catalog
Section titled “Built-in middleware catalog”All packages are under github.com/spcent/plumego/middleware/.
| Package | Constructor | Typical usage |
|---|---|---|
requestid | requestid.Middleware() | First in chain. Generates or passes through X-Request-ID; stores ID in context via contract.RequestIDFromContext. |
recovery | recovery.Middleware(recovery.Config{Logger: logger}) | Second in chain. Catches panics, logs sanitized panic metadata, returns 500. |
accesslog | accesslog.Middleware(accesslog.Config{Logger: logger}) | Third in chain. Logs method, path, status code, and latency for every request. |
auth | authmw.Authenticate(authenticator) | Constructs auth middleware with startup validation. Runtime failures return 401 or 403. |
security | secmw.Middleware(secmw.Config{Policy: &policy}) | Sets hardening headers: CSP, X-Frame-Options, HSTS, X-Content-Type-Options. |
cors | cors.Middleware(opts) | Handles preflight and adds CORS response headers. |
timeout | timeout.Middleware(cfg) | Cancels the request context after the configured duration; returns 504. |
ratelimit | ratelimit.AbuseGuard(cfg) | Token-bucket rate limiter per IP. Returns 429 with Retry-After. |
bodylimit | bodylimit.BodyLimit(maxBytes, logger) | Rejects requests whose body exceeds the limit with 413. |
compression | compression.Middleware(cfg) | Gzip-compresses responses for clients that accept gzip. |
httpmetrics | httpmetrics.Middleware(observer) | Records request count, status distribution, and latency via a metrics.HTTPObserver. |
tracing | tracing.Middleware(tracer) | Injects or propagates a distributed trace context. |
concurrencylimit | concurrencylimit.Middleware(max, queue, timeout) | Caps concurrent in-flight requests; queues up to queue depth, rejects with 503 beyond. |
debug | debug.Middleware(cfg) | Dev-mode error capture. Do not use in production. |
coalesce | coalesce.New(cfg).Middleware() | Request coalescing — deduplicates identical concurrent requests, returning one shared response. |
conformance | conformance.Middleware(...) | Validates requests and responses against declared API contracts. |
Recommended ordering
Section titled “Recommended ordering”Stack order matters. Follow this sequence for a typical API service:
app.Use(requestid.Middleware()) // 1. ID before everythingrecoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})if err != nil { return err}app.Use(recoveryMw) // 2. panic recoveryaccesslogMw, 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 lessapp.Use(cors.Middleware(corsOpts)) // 4. CORS headers before any auth checkpolicy := headers.DefaultPolicy()securityMw, err := secmw.Middleware(secmw.Config{Policy: &policy})if err != nil { return err}app.Use(securityMw) // 5. security headersapp.Use(rl.AbuseGuard(rl.DefaultAbuseGuardConfig())) // 6. rate limit before authapp.Use(to.Middleware(to.Config{Timeout: 10 * time.Second})) // 7. timeoutauthMw, err := authmw.Authenticate(authenticator)if err != nil { return err}app.Use(authMw) // 8. auth last in the default stackWrite a custom middleware
Section titled “Write a custom middleware”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.