Middleware Patterns
Middleware Patterns
Overview
Middleware wraps http.Handler to add cross-cutting behavior: logging, auth, recovery, request IDs, CORS, metrics. Keep each piece small and composable.
The shape
type Middleware func(http.Handler) http.Handler
func chain(h http.Handler, ms ...Middleware) http.Handler {
for i := len(ms) - 1; i >= 0; i-- {
h = ms[i](h)
}
return h
}request → recover → requestID → log → auth → mux → handler
response ←─────────────────────────────────────────────
Order matters: recover outermost so panics in other middleware still convert to 500.
Recover
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
slog.Error("panic", "err", rec, "path", r.URL.Path)
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}Request ID
type ctxKey int
const reqIDKey ctxKey = 1
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = strconv.FormatInt(time.Now().UnixNano(), 36)
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), reqIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Access log with status
Capture status by wrapping ResponseWriter:
type statusWriter struct {
http.ResponseWriter
code int
}
func (w *statusWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
func AccessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w, code: 200}
start := time.Now()
next.ServeHTTP(sw, r)
slog.Info("http",
"method", r.Method,
"path", r.URL.Path,
"status", sw.code,
"ms", time.Since(start).Milliseconds(),
)
})
}Implement Unwrap() / Flush if you need http.Hijacker or Flusher passthrough in production.
Max body
func MaxBytes(n int64) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, n)
next.ServeHTTP(w, r)
})
}
}Timeout (handler budget)
func Timeout(d time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, d, "timeout")
}
}Prefer also setting http.Server timeouts (chapter 271).
Rules of thumb
| Do | Don’t |
|---|---|
| One concern per middleware | Mega-middleware that logs+auth+metrics+… |
| Document chain order | Auth after handler “for speed” |
| Propagate context values | Global request maps without locks |
Try next
- Build a chain: Recover → RequestID → AccessLog → mux.
- Add unit tests with
httptestasserting status on panic.
- Log request ID from a handler via context.