# Middleware Pattern in Go ## Pattern Overview The Middleware Pattern wraps HTTP handlers to add cross-cutting concerns like logging, authentication, error recovery, and request preprocessing. It follows the decorator pattern, allowing you to compose multiple middleware into a chain. ## Pattern Structure ```go // Middleware function signature type Middleware func(http.Handler) http.Handler // Middleware wraps a handler func MyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Pre-processing (before handler) // ... do something before // Call next handler next.ServeHTTP(w, r) // Post-processing (after handler) // ... do something after }) } ``` ## Real Implementation from Project ### Preferences Middleware ```go // internal/middleware/preferences.go // PreferencesMiddleware reads user preference cookies and stores them in request context func PreferencesMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Pre-processing: Read cookies prefs := &Preferences{ CVLength: getCookieWithDefault(r, "cv-length", "short"), CVIcons: getCookieWithDefault(r, "cv-icons", "show"), CVLanguage: getCookieWithDefault(r, "cv-language", "en"), CVTheme: getCookieWithDefault(r, "cv-theme", "default"), ColorTheme: getCookieWithDefault(r, "color-theme", "light"), } // Migrate old values if prefs.CVLength == "extended" { prefs.CVLength = "long" } // Store in context ctx := context.WithValue(r.Context(), PreferencesKey, prefs) // Call next handler with modified context next.ServeHTTP(w, r.WithContext(ctx)) // No post-processing needed for this middleware }) } ``` ### Recovery Middleware ```go // internal/middleware/recovery.go // Recovery catches panics and returns 500 error func Recovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Setup panic recovery defer func() { if err := recover(); err != nil { // Log panic with stack trace log.Printf("PANIC: %v\n%s", err, debug.Stack()) // Return error response http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() // Call next handler (protected by defer/recover) next.ServeHTTP(w, r) }) } ``` ### Logger Middleware ```go // internal/middleware/logger.go // Logger logs HTTP requests and their duration func Logger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Pre-processing: Start timer and log request start := time.Now() log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr) // Wrap ResponseWriter to capture status code wrapped := &responseWriter{ ResponseWriter: w, statusCode: http.StatusOK, } // Call next handler next.ServeHTTP(wrapped, r) // Post-processing: Log duration and status duration := time.Since(start) log.Printf("Completed in %v (status: %d)", duration, wrapped.statusCode) }) } // Helper to capture response status type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } ``` ## Middleware Composition ### Chaining Middleware ```go // internal/routes/routes.go func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler { mux := http.NewServeMux() // Register routes mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/cv", cvHandler.CVContent) mux.HandleFunc("/health", healthHandler.Health) // Compose middleware chain // Execution order: Recovery → Logger → SecurityHeaders → Preferences → mux handler := middleware.Recovery( middleware.Logger( middleware.SecurityHeaders( middleware.PreferencesMiddleware(mux), ), ), ) return handler } ``` ### Route-Specific Middleware ```go // Apply middleware only to specific routes func Setup(cvHandler *handlers.CVHandler) http.Handler { mux := http.NewServeMux() // Public routes (minimal middleware) mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/health", healthHandler.Health) // Protected PDF route (additional middleware) pdfHandler := middleware.OriginChecker( middleware.RateLimiter( http.HandlerFunc(cvHandler.ExportPDF), 3, // 3 requests per minute ), ) mux.Handle("/export/pdf", pdfHandler) // Global middleware for all routes handler := middleware.Recovery( middleware.Logger( middleware.PreferencesMiddleware(mux), ), ) return handler } ``` ## Common Middleware Use Cases ### 1. Authentication ```go func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get token from header token := r.Header.Get("Authorization") // Validate token userID, err := validateToken(token) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Store user ID in context ctx := context.WithValue(r.Context(), UserIDKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) } ``` ### 2. CORS ```go func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") // Handle preflight if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } ``` ### 3. Request Timeout ```go func Timeout(duration time.Duration) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Create context with timeout ctx, cancel := context.WithTimeout(r.Context(), duration) defer cancel() // Create channel for handler completion done := make(chan struct{}) go func() { next.ServeHTTP(w, r.WithContext(ctx)) close(done) }() // Wait for completion or timeout select { case <-done: // Handler completed case <-ctx.Done(): // Timeout occurred http.Error(w, "Request Timeout", http.StatusGatewayTimeout) } }) } } ``` ### 4. Request ID ```go func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Generate unique ID requestID := uuid.New().String() // Add to response header w.Header().Set("X-Request-ID", requestID) // Store in context ctx := context.WithValue(r.Context(), RequestIDKey, requestID) next.ServeHTTP(w, r.WithContext(ctx)) }) } ``` ## Middleware Execution Flow ``` Request │ ▼ ┌─────────────────────────┐ │ Recovery Middleware │ ← Outermost (catches all panics) │ defer/recover │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Logger Middleware │ ← Logs request + duration │ Pre: Log request │ │ Post: Log duration │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Security Middleware │ ← Add security headers │ Set headers │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Preferences Middleware │ ← Innermost (closest to handler) │ Read cookies → context │ └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ Handler │ ← Business logic │ Process request │ └─────────────────────────┘ │ ▼ Response (unwraps in reverse order) ``` ## Benefits 1. **Separation of Concerns**: Cross-cutting logic separate from handlers 2. **Composability**: Chain multiple middleware together 3. **Reusability**: Same middleware for multiple routes 4. **Testability**: Easy to test in isolation 5. **Maintainability**: Change behavior without touching handlers ## Best Practices ### ✅ DO ```go // Keep middleware focused on one concern func LoggerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Only logging logic here log.Printf("[%s] %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) }) } // Use context for request-scoped values func PreferencesMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { prefs := readPreferences(r) ctx := context.WithValue(r.Context(), PrefsKey, prefs) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Order middleware correctly (outer to inner) handler := Recovery(Logger(Auth(mux))) ``` ### ❌ DON'T ```go // DON'T mix multiple concerns in one middleware func BadMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Too much! Logging, auth, CORS, caching... log.Print(r.URL) if !checkAuth(r) { return } w.Header().Set("Access-Control-Allow-Origin", "*") cached := getCache(r.URL.Path) // ... }) } // DON'T store context in struct type BadMiddleware struct { ctx context.Context // Wrong! } // DON'T modify original request (use r.WithContext) func BadMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Header.Set("X-Foo", "bar") // Modifies original! next.ServeHTTP(w, r) }) } ``` ## Testing Middleware ```go func TestPreferencesMiddleware(t *testing.T) { // Create test handler that reads preferences handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { prefs := GetPreferences(r) if prefs.CVLength != "long" { t.Errorf("expected long, got %s", prefs.CVLength) } w.WriteHeader(http.StatusOK) }) // Wrap with middleware wrapped := PreferencesMiddleware(handler) // Create test request with cookie req := httptest.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{Name: "cv-length", Value: "long"}) // Execute w := httptest.NewRecorder() wrapped.ServeHTTP(w, req) // Verify if w.Code != http.StatusOK { t.Errorf("expected 200, got %d", w.Code) } } ``` ## Related Patterns - **Chain of Responsibility**: Middleware is a specific implementation - **Decorator Pattern**: Wrapping handlers adds behavior - **Context Pattern**: Often used together for request-scoped data ## Further Reading - [Writing Middleware in Go](https://www.alexedwards.net/blog/making-and-using-middleware) - [Middleware Pattern in Go](https://gowebexamples.com/advanced-middleware/) - [Context Pattern](./03-context-pattern.md) - Used with middleware