d95c62bad4
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
12 KiB
12 KiB
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
// 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
// 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
// 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
// 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
// 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
// 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
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
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
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
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
- Separation of Concerns: Cross-cutting logic separate from handlers
- Composability: Chain multiple middleware together
- Reusability: Same middleware for multiple routes
- Testability: Easy to test in isolation
- Maintainability: Change behavior without touching handlers
Best Practices
✅ DO
// 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
// 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
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
- Middleware Pattern in Go
- Context Pattern - Used with middleware