426 lines
12 KiB
Markdown
426 lines
12 KiB
Markdown
|
|
# 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
|