middleware

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 15, 2026 License: MIT Imports: 15 Imported by: 0

README

Middleware Package

HTTP middleware components for Nivo services, providing common functionality like logging, panic recovery, CORS handling, request IDs, and timeouts.

Features

  • Middleware Chaining: Compose multiple middleware in a clean, readable way
  • Request/Response Logging: Structured logging with timing and status tracking
  • Panic Recovery: Gracefully handle panics with stack trace logging
  • CORS: Flexible CORS configuration for cross-origin requests
  • Request ID: Generate or extract request IDs for request tracing
  • Timeout: Enforce request timeouts with context cancellation
  • Response Writer: Capture status codes and response sizes

Installation

go get github.com/1mb-dev/nivomoney/shared/middleware

Usage

Basic Middleware Chain
package main

import (
    "net/http"
    "time"

    "github.com/1mb-dev/nivomoney/shared/logger"
    "github.com/1mb-dev/nivomoney/shared/middleware"
)

func main() {
    log := logger.NewDefault("api")

    // Your handler
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    // Chain middleware
    wrapped := middleware.Chain(
        handler,
        middleware.Recovery(log),              // First: catch panics
        middleware.RequestID(),                // Second: add request ID
        middleware.Logging(log),               // Third: log requests
        middleware.CORS(middleware.DefaultCORSConfig()),
        middleware.Timeout(30 * time.Second),
    )

    http.ListenAndServe(":8080", wrapped)
}
Request ID

Generate unique request IDs for tracing requests across services:

// Automatically generates request ID if not present
app := middleware.Chain(
    handler,
    middleware.RequestID(),
)

// In your handler, access the request ID from context
func myHandler(w http.ResponseWriter, r *http.Request) {
    requestID := r.Context().Value(logger.RequestIDKey)
    log.WithField("request_id", requestID).Info("processing request")
}

Request IDs are:

  • Extracted from X-Request-ID header if present
  • Generated as 32-character hex strings if not provided
  • Added to response headers for client tracking
  • Available in context for downstream use
Logging

Log all HTTP requests and responses with timing information:

log := logger.New(logger.Config{
    Level:       "info",
    Format:      "json",
    ServiceName: "api",
})

app := middleware.Chain(
    handler,
    middleware.RequestID(),  // Add this first for request ID in logs
    middleware.Logging(log),
)

Logged information includes:

  • Request: method, path, remote address, request ID
  • Response: status code, bytes written, duration in milliseconds

Example log output:

{
  "level": "info",
  "request_id": "abc123",
  "method": "GET",
  "path": "/users",
  "remote_addr": "192.168.1.1:12345",
  "message": "request started"
}
{
  "level": "info",
  "request_id": "abc123",
  "status": 200,
  "bytes": 1024,
  "duration_ms": 45,
  "message": "request completed"
}
Recovery

Recover from panics gracefully and log with stack traces:

log := logger.NewDefault("api")

// Basic recovery with default error response
app := middleware.Chain(
    handler,
    middleware.Recovery(log),
)

// Custom panic handler
customHandler := func(w http.ResponseWriter, r *http.Request, err interface{}) {
    log.WithField("panic", err).Error("custom panic handler")
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(`{"error": "something went wrong"}`))
}

app := middleware.Chain(
    handler,
    middleware.RecoveryWithHandler(log, customHandler),
)

The recovery middleware:

  • Catches panics and prevents server crashes
  • Logs panic value, stack trace, request method, and path
  • Returns 500 Internal Server Error with JSON response
  • Allows custom panic handlers for specialized recovery logic
CORS

Handle Cross-Origin Resource Sharing (CORS) requests:

// Use default configuration (permissive, for development)
app := middleware.Chain(
    handler,
    middleware.CORS(middleware.DefaultCORSConfig()),
)

// Custom configuration
config := middleware.CORSConfig{
    AllowedOrigins:   []string{"https://example.com", "https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowedHeaders:   []string{"Authorization", "Content-Type", "X-Request-ID"},
    ExposedHeaders:   []string{"X-Request-ID", "X-Total-Count"},
    AllowCredentials: true,
    MaxAge:           3600, // 1 hour
}

app := middleware.Chain(
    handler,
    middleware.CORS(config),
)

CORS middleware features:

  • Wildcard origin support (["*"]) for development
  • Specific origin allowlist for production
  • Automatic preflight (OPTIONS) request handling
  • Configurable methods, headers, and credentials
  • Cache control via MaxAge

Production Best Practice:

// Production CORS config
config := middleware.CORSConfig{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
    AllowedHeaders:   []string{"Authorization", "Content-Type"},
    ExposedHeaders:   []string{"X-Request-ID"},
    AllowCredentials: true,
    MaxAge:           86400, // 24 hours
}
Timeout

Enforce request timeouts to prevent long-running handlers:

// 30 second timeout
app := middleware.Chain(
    handler,
    middleware.Timeout(30 * time.Second),
)

// Different timeouts for different routes
apiHandler := middleware.Chain(
    apiRouter,
    middleware.Timeout(10 * time.Second),
)

uploadHandler := middleware.Chain(
    uploadRouter,
    middleware.Timeout(5 * time.Minute),
)

Timeout middleware:

  • Uses context cancellation for clean timeout handling
  • Returns 504 Gateway Timeout when timeout occurs
  • Handlers should check r.Context().Done() for cancellation
  • Handler continues in background if it doesn't check context

Handler Best Practice:

func slowHandler(w http.ResponseWriter, r *http.Request) {
    for {
        select {
        case <-r.Context().Done():
            // Context cancelled (timeout or client disconnect)
            return
        default:
            // Do work
            time.Sleep(100 * time.Millisecond)
        }
    }
}
Response Writer

The ResponseWriter wrapper is used internally by logging middleware to capture response metadata:

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    rw := middleware.NewResponseWriter(w)

    // Use wrapped writer
    rw.WriteHeader(http.StatusCreated)
    rw.Write([]byte("created"))

    // Access captured metadata
    statusCode := rw.StatusCode     // 201
    bytes := rw.BytesWritten        // 7
})

Features:

  • Captures HTTP status code (defaults to 200)
  • Tracks bytes written
  • Prevents multiple WriteHeader calls
  • Transparent proxy for all other operations

Middleware Ordering

The order of middleware matters! They execute in the order provided to Chain:

app := middleware.Chain(
    handler,
    middleware.Recovery(log),      // 1. Outermost: catch panics from all below
    middleware.RequestID(),        // 2. Generate request ID early
    middleware.Logging(log),       // 3. Log after request ID is available
    middleware.CORS(config),       // 4. Handle CORS before business logic
    middleware.Timeout(30*time.Second), // 5. Innermost: enforce timeout on handler
)

Execution flow (request → response):

  1. Recovery middleware starts (deferred panic handler)
  2. RequestID generates/extracts ID
  3. Logging logs "request started"
  4. CORS sets headers
  5. Timeout starts timer
  6. Handler executes
  7. Timeout clears timer
  8. CORS processes response
  9. Logging logs "request completed"
  10. Recovery completes (no panic)

Testing

All middleware components have comprehensive test coverage (98.9%):

# Run tests
cd shared/middleware
go test -v

# Run tests with coverage
go test -cover

# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Examples

Complete API Server
package main

import (
    "context"
    "net/http"
    "time"

    "github.com/1mb-dev/nivomoney/shared/logger"
    "github.com/1mb-dev/nivomoney/shared/middleware"
)

func main() {
    // Initialize logger
    log := logger.New(logger.Config{
        Level:       "info",
        Format:      "json",
        ServiceName: "api",
    })

    // Create router
    mux := http.NewServeMux()

    // Add routes
    mux.HandleFunc("/health", healthHandler)
    mux.HandleFunc("/api/users", usersHandler)

    // Configure CORS for production
    corsConfig := middleware.CORSConfig{
        AllowedOrigins:   []string{"https://app.example.com"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders:   []string{"Authorization", "Content-Type", "X-Request-ID"},
        ExposedHeaders:   []string{"X-Request-ID"},
        AllowCredentials: true,
        MaxAge:           3600,
    }

    // Build middleware stack
    handler := middleware.Chain(
        mux,
        middleware.Recovery(log),
        middleware.RequestID(),
        middleware.Logging(log),
        middleware.CORS(corsConfig),
        middleware.Timeout(30 * time.Second),
    )

    // Start server
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    log.Info("starting server on :8080")
    if err := server.ListenAndServe(); err != nil {
        log.WithError(err).Fatal("server failed")
    }
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "healthy"}`))
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    // Get request ID from context
    requestID := r.Context().Value(logger.RequestIDKey)

    // Check for timeout
    select {
    case <-r.Context().Done():
        return // Request cancelled
    default:
        // Process request
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"users": []}`))
    }
}
Custom Middleware

Create your own middleware following the same pattern:

func RateLimiter(limit int) middleware.Middleware {
    limiter := rate.NewLimiter(rate.Limit(limit), limit)

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                w.WriteHeader(http.StatusTooManyRequests)
                w.Write([]byte(`{"error": "rate limit exceeded"}`))
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// Use it
app := middleware.Chain(
    handler,
    middleware.Recovery(log),
    RateLimiter(100), // Custom middleware
    middleware.Logging(log),
)

Best Practices

  1. Always use Recovery first to catch panics from all other middleware
  2. Generate RequestID early so it's available for logging
  3. Use structured logging to track requests across services
  4. Check context cancellation in long-running handlers
  5. Configure CORS restrictively in production
  6. Set appropriate timeouts based on endpoint characteristics
  7. Test middleware chains to verify expected behavior
  8. Use DefaultCORSConfig only in development

Performance

The middleware stack adds minimal overhead:

  • Request ID generation: ~100ns
  • Logging: ~1-2µs per log line
  • CORS: ~500ns
  • Recovery (no panic): ~50ns
  • Timeout (completes normally): ~1µs
  • ResponseWriter wrapping: ~10ns

Total overhead for typical stack: ~5-10µs per request

Architecture

Request
  ↓
Recovery (catch panics)
  ↓
RequestID (generate/extract ID)
  ↓
Logging (log request start)
  ↓
CORS (set headers)
  ↓
Timeout (start timer)
  ↓
Handler (your code)
  ↓
Timeout (clear timer)
  ↓
CORS (process response)
  ↓
Logging (log request completion)
  ↓
Recovery (complete)
  ↓
Response

License

Copyright © 2025 Nivo

Documentation

Overview

Package middleware provides common HTTP middleware for Nivo services.

Index

Constants

View Source
const (
	// CSRFTokenHeader is the header name for CSRF token.
	CSRFTokenHeader = "X-CSRF-Token" //nolint:gosec // Not a credential, just a header name
	// CSRFCookieName is the cookie name for CSRF token.
	CSRFCookieName = "csrf_token"
	// CSRFTokenLength is the length of the CSRF token in bytes (32 bytes = 64 hex chars).
	CSRFTokenLength = 32
)

Variables

This section is empty.

Functions

func Auth

func Auth(config AuthConfig) func(http.Handler) http.Handler

Auth creates a middleware that validates JWT tokens and extracts user claims.

func CSRFTokenFromRequest

func CSRFTokenFromRequest(r *http.Request) string

CSRFTokenFromRequest extracts the CSRF token from the request cookie. This can be used by handlers that need to include the token in responses.

func Chain

func Chain(handler http.Handler, middleware ...Middleware) http.Handler

Chain applies multiple middleware in order. Middleware are applied in the order they are provided.

func GetAccountType

func GetAccountType(ctx context.Context) (string, bool)

GetAccountType extracts the account type from the request context.

func GetUserEmail

func GetUserEmail(ctx context.Context) (string, bool)

GetUserEmail extracts the user email from the request context.

func GetUserID

func GetUserID(ctx context.Context) (string, bool)

GetUserID extracts the user ID from the request context.

func GetUserPermissions

func GetUserPermissions(ctx context.Context) ([]string, bool)

GetUserPermissions extracts the user permissions from the request context.

func GetUserRoles

func GetUserRoles(ctx context.Context) ([]string, bool)

GetUserRoles extracts the user roles from the request context.

func GetUserStatus

func GetUserStatus(ctx context.Context) (string, bool)

GetUserStatus extracts the user status from the request context.

func InternalAuth

func InternalAuth(config InternalAuthConfig) func(http.Handler) http.Handler

InternalAuth creates a middleware that validates service-to-service requests using a shared secret header. This protects internal endpoints from external access.

func InternalAuthFunc

func InternalAuthFunc(secret string, next http.HandlerFunc) http.HandlerFunc

InternalAuthFunc is a simpler version that returns a http.HandlerFunc wrapper. Use this for registering with mux.HandleFunc.

func RateLimit

func RateLimit(config RateLimitConfig) func(http.Handler) http.Handler

RateLimit creates a rate limiting middleware with the given configuration

func RequireAnyPermission

func RequireAnyPermission(permissions ...string) func(http.Handler) http.Handler

RequireAnyPermission creates a middleware that checks if the user has ANY of the required permissions.

func RequireAnyRole

func RequireAnyRole(roles ...string) func(http.Handler) http.Handler

RequireAnyRole creates a middleware that checks if the user has ANY of the required roles.

func RequirePermission

func RequirePermission(permission string) func(http.Handler) http.Handler

RequirePermission creates a middleware that checks if the user has the required permission.

func RequireRole

func RequireRole(role string) func(http.Handler) http.Handler

RequireRole creates a middleware that checks if the user has the required role.

Types

type AuthConfig

type AuthConfig struct {
	JWTSecret string
	// Optional: Skip auth for certain paths
	SkipPaths []string
}

AuthConfig holds configuration for auth middleware.

type CORSConfig

type CORSConfig struct {
	AllowedOrigins   []string // List of allowed origins, or ["*"] for all
	AllowedMethods   []string // HTTP methods (GET, POST, etc.)
	AllowedHeaders   []string // HTTP headers
	ExposedHeaders   []string // Headers exposed to client
	AllowCredentials bool     // Allow credentials
	MaxAge           int      // Preflight cache duration in seconds
}

CORSConfig holds CORS configuration.

func DefaultCORSConfig

func DefaultCORSConfig() CORSConfig

DefaultCORSConfig returns a restrictive CORS configuration. For production, explicitly configure AllowedOrigins via environment variables.

type CSRFConfig

type CSRFConfig struct {
	// SkipPaths are paths that don't require CSRF validation (e.g., login, register).
	SkipPaths []string
	// CookiePath is the path for the CSRF cookie. Default is "/".
	CookiePath string
	// CookieSecure sets the Secure flag on the cookie. Should be true in production.
	CookieSecure bool
	// CookieSameSite sets the SameSite attribute. Default is SameSiteLaxMode.
	CookieSameSite http.SameSite
}

CSRFConfig holds configuration for CSRF middleware.

func DefaultCSRFConfig

func DefaultCSRFConfig() CSRFConfig

DefaultCSRFConfig returns a default CSRF configuration.

type ContextKey

type ContextKey string

ContextKey is a custom type for context keys to avoid collisions.

const (
	// UserIDKey is the context key for user ID.
	UserIDKey ContextKey = "user_id"
	// UserEmailKey is the context key for user email.
	UserEmailKey ContextKey = "user_email"
	// UserStatusKey is the context key for user status.
	UserStatusKey ContextKey = "user_status"
	// UserRolesKey is the context key for user roles.
	UserRolesKey ContextKey = "user_roles"
	// UserPermissionsKey is the context key for user permissions.
	UserPermissionsKey ContextKey = "user_permissions"
	// JWTTokenKey is the context key for the JWT token string (for service-to-service forwarding).
	JWTTokenKey ContextKey = "jwt_token"
	// AccountTypeKey is the context key for account type (user, user_admin).
	AccountTypeKey ContextKey = "account_type"
)

type InternalAuthConfig

type InternalAuthConfig struct {
	// Secret is the shared secret for internal service communication.
	// Should be set via INTERNAL_SERVICE_SECRET environment variable.
	Secret string
	// HeaderName is the header to check for the secret. Defaults to X-Internal-Secret.
	HeaderName string
}

InternalAuthConfig holds configuration for internal service-to-service auth.

type JWTClaims

type JWTClaims struct {
	UserID      string   `json:"user_id"`
	Email       string   `json:"email"`
	Status      string   `json:"status"`
	AccountType string   `json:"account_type,omitempty"`
	Roles       []string `json:"roles,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
	jwt.RegisteredClaims
}

JWTClaims represents the JWT token claims structure.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is a function that wraps an http.Handler.

func CORS

func CORS(config CORSConfig) Middleware

CORS returns a middleware that handles CORS requests.

func CSRF

func CSRF(config CSRFConfig) Middleware

CSRF creates a middleware that implements CSRF protection using double-submit cookie pattern.

How it works: 1. On any request, if no CSRF cookie exists, generate one and set it 2. On mutating requests (POST, PUT, DELETE, PATCH), validate that X-CSRF-Token header matches cookie 3. Frontend reads the csrf_token cookie and sends it in X-CSRF-Token header

This is a stateless approach that doesn't require server-side session storage.

func Logging

func Logging(log *logger.Logger) Middleware

Logging returns a middleware that logs HTTP requests and responses.

func Recovery

func Recovery(log *logger.Logger) Middleware

Recovery returns a middleware that recovers from panics and logs them.

func RecoveryWithHandler

func RecoveryWithHandler(log *logger.Logger, handler func(http.ResponseWriter, *http.Request, interface{})) Middleware

RecoveryWithHandler returns a middleware that recovers from panics and calls a custom handler.

func RequestID

func RequestID() Middleware

RequestID returns a middleware that generates or extracts request IDs.

func Timeout

func Timeout(duration time.Duration) Middleware

Timeout returns a middleware that sets a timeout for requests.

type RateLimitConfig

type RateLimitConfig struct {
	// RequestsPerMinute is the maximum number of requests allowed per minute
	RequestsPerMinute int

	// BurstSize is the maximum burst of requests allowed
	BurstSize int

	// CleanupInterval is how often to clean up expired entries (default: 10 minutes)
	CleanupInterval time.Duration

	// TrustProxyHeaders enables trusting X-Forwarded-For and X-Real-IP headers.
	// SECURITY: Only enable this when behind a trusted reverse proxy (nginx, traefik, etc.)
	// that properly sets these headers. Leaving this false prevents IP spoofing attacks.
	TrustProxyHeaders bool
}

RateLimitConfig holds configuration for rate limiting

func DefaultRateLimitConfig

func DefaultRateLimitConfig() RateLimitConfig

DefaultRateLimitConfig returns a sensible default configuration Suitable for authentication endpoints (prevent brute force)

func StrictRateLimitConfig

func StrictRateLimitConfig() RateLimitConfig

StrictRateLimitConfig returns a stricter configuration Suitable for sensitive operations (KYC verification, money transfers)

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	StatusCode   int
	BytesWritten int
	// contains filtered or unexported fields
}

ResponseWriter wraps http.ResponseWriter to capture status code and size.

func NewResponseWriter

func NewResponseWriter(w http.ResponseWriter) *ResponseWriter

NewResponseWriter creates a new ResponseWriter.

func (*ResponseWriter) Flush

func (rw *ResponseWriter) Flush()

Flush implements http.Flusher to support streaming responses like SSE.

func (*ResponseWriter) Write

func (rw *ResponseWriter) Write(b []byte) (int, error)

Write captures the number of bytes written.

func (*ResponseWriter) WriteHeader

func (rw *ResponseWriter) WriteHeader(statusCode int)

WriteHeader captures the status code.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL