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.