Files
juanatsap d95c62bad4 refactor: remove outdated server design documentation
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.
2025-12-02 20:25:05 +00:00

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

  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

// 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)
    }
}
  • 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