Day 63 — HTTP Middleware
Learning Objectives
- Build a middleware chain for the security proxy
- Capture status codes and response size without breaking the handler interface
- Implement panic recovery — prevents one request from crashing the whole server
Exercise: Security Middleware Chain
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"runtime/debug"
"time"
)
// statusCapture wraps ResponseWriter to capture status code written by handler
type statusCapture struct {
http.ResponseWriter
statusCode int
bytesWritten int64
headersSent bool
}
func (sc *statusCapture) WriteHeader(code int) {
sc.statusCode = code
sc.headersSent = true
sc.ResponseWriter.WriteHeader(code)
}
func (sc *statusCapture) Write(b []byte) (int, error) {
if !sc.headersSent {
sc.WriteHeader(http.StatusOK)
}
n, err := sc.ResponseWriter.Write(b)
sc.bytesWritten += int64(n)
return n, err
}
// Chain executes middlewares in order (first middleware is outermost)
func Chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// PanicRecoverer catches panics in handlers and returns 500
func PanicRecoverer(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logger.Error("handler panic",
"error", fmt.Sprintf("%v", rec),
"stack", string(debug.Stack()),
"path", r.URL.Path,
)
// Only write 500 if headers not yet sent
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"error":"internal server error"}`)
}
}()
next.ServeHTTP(w, r)
})
}
}
// RequestLogger logs each request as structured JSON
func RequestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
capture := &statusCapture{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(capture, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", capture.statusCode,
"bytes", capture.bytesWritten,
"latency_ms", time.Since(start).Milliseconds(),
"ip", r.RemoteAddr,
"user_agent", r.UserAgent(),
)
})
}
}
// SecurityHeaders sets security response headers on all responses
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("X-XSS-Protection", "0") // modern browsers: use CSP instead
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("Permissions-Policy", "geolocation=(), camera=(), microphone=()")
// HSTS: only set on HTTPS responses
if r.TLS != nil {
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
}
next.ServeHTTP(w, r)
})
}
// RequestID injects a unique ID into the request context and response header
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := generateRequestID()
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
logger := slog.Default()
appHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/panic" {
panic("deliberate panic for testing")
}
fmt.Fprintf(w, `{"status":"ok","path":%q}`, r.URL.Path)
})
// Middleware order (outermost first):
// PanicRecoverer → RequestID → SecurityHeaders → Logger → handler
handler := Chain(appHandler,
PanicRecoverer(logger),
RequestID,
SecurityHeaders,
RequestLogger(logger),
)
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}
fmt.Println("Server with middleware chain on :8080")
fmt.Println("Test panic recovery: curl http://localhost:8080/panic")
srv.ListenAndServe()
}
Real-World Context
Middleware ordering is a security decision: PanicRecoverer must be outermost (catches panics in all downstream middleware); RequestID before Logger (so logs include the request ID); SecurityHeaders unconditionally (always set, regardless of handler outcome). OWASP A05 — missing security headers is a common audit finding.