Skip to content

Middleware Chain

Onion Model

HTTPC middleware follows an onion model -- requests go from outer to inner, responses from inner to outer:

text
Request ->  Recovery  ->  Logging  ->  RequestID  -> Handler
                                                              |
Response <-  Recovery  <-  Logging  <-  RequestID  <- Response
go
cfg := httpc.DefaultConfig()
cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
    httpc.RecoveryMiddleware(),    // Outermost: panic recovery
    httpc.LoggingMiddleware(log.Printf), // Second layer: logging
    httpc.RequestIDMiddleware("X-Request-ID", nil), // Innermost: request ID
}

client, _ := httpc.New(cfg)

Built-in Middleware

RecoveryMiddleware

Panic recovery to prevent process crashes:

go
httpc.RecoveryMiddleware()

LoggingMiddleware

Request/response logging with automatic URL masking:

go
httpc.LoggingMiddleware(func(format string, args ...any) {
    log.Printf("[HTTP] "+format, args...)
})
// Output example: [HTTP] GET https://api.example.com/data -> 200 (150ms) (status code and duration are measured values, not fixed)

RequestIDMiddleware

Adds a unique ID to each request, generated with crypto/rand:

go
httpc.RequestIDMiddleware("X-Request-ID", nil) // Default 32-char hex

// Custom generator
httpc.RequestIDMiddleware("X-Request-ID", func() string {
    return uuid.New().String()
})

TimeoutMiddleware

Middleware-level timeout enforced before the client timeout:

go
httpc.TimeoutMiddleware(30 * time.Second)

Do not use for Download or streaming requests

TimeoutMiddleware's defer cancel() fires immediately after the handler returns (i.e., once the response headers are received), so for Download or WithStreamBody requests it cancels the context before the response body is read, producing a "context canceled" error. For streaming/download scenarios, use the WithTimeout option instead.

HeaderMiddleware

Adds static headers to all requests:

go
httpc.HeaderMiddleware(map[string]string{
    "X-App-Version": "1.0.0",
    "X-Platform":    "server",
})

MetricsMiddleware

Collects request metrics:

go
httpc.MetricsMiddleware(func(method, url string, statusCode int, duration time.Duration, err error) {
    metrics.IncrCounter("http.requests", 1)
    metrics.RecordTimer("http.latency", duration)
    if err != nil {
        metrics.IncrCounter("http.errors", 1)
    }
})

AuditMiddleware

Security auditing for financial, medical, and other compliance scenarios:

go
httpc.AuditMiddleware(func(event httpc.AuditEvent) {
    log.Printf("[AUDIT] %s %s -> %d (%v)",
        event.Method, event.URL, event.StatusCode, event.Duration)
})

AuditMiddlewareWithConfig

Configurable audit middleware:

go
auditCfg := &httpc.AuditMiddlewareConfig{
    Format:         "json",
    IncludeHeaders: true,
    MaskHeaders:    []string{"Authorization", "Cookie"},
    SanitizeError:  true,
}

httpc.AuditMiddlewareWithConfig(func(event httpc.AuditEvent) {
    data, _ := json.Marshal(event)
    log.Println(string(data))
}, auditCfg)

Audit events support extracting SourceIP and UserID from context:

go
ctx := context.WithValue(context.Background(), httpc.SourceIPKey, "192.168.1.1")
ctx = context.WithValue(ctx, httpc.UserIDKey, "user-123")

Manual Chain Composition

Use the Chain function to compose middleware:

go
middleware := httpc.Chain(
    httpc.RecoveryMiddleware(),
    httpc.LoggingMiddleware(log.Printf),
    httpc.RequestIDMiddleware("X-Request-ID", nil),
)

cfg := httpc.DefaultConfig()
cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{middleware}

Custom Middleware

go
func CORSMiddleware(origin string) httpc.MiddlewareFunc {
    return func(next httpc.Handler) httpc.Handler {
        return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
            // Request phase: modify request
            req.SetHeader("Origin", origin)

            // Call next handler
            resp, err := next(ctx, req)

            // Response phase: log or modify response
            if resp != nil {
                log.Printf("Response status: %d", resp.StatusCode())
            }

            return resp, err
        }
    }
}

Short-Circuit Middleware

go
func CircuitBreakerMiddleware(threshold int) httpc.MiddlewareFunc {
    var failures int
    var mu sync.Mutex

    return func(next httpc.Handler) httpc.Handler {
        return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
            mu.Lock()
            if failures >= threshold {
                mu.Unlock()
                return nil, fmt.Errorf("circuit breaker open")
            }
            mu.Unlock()

            resp, err := next(ctx, req)
            if err != nil {
                mu.Lock()
                failures++
                mu.Unlock()
            }
            return resp, err
        }
    }
}

Middleware Configuration

go
cfg := httpc.DefaultConfig()
cfg.Middleware = &httpc.MiddlewareConfig{
    Middlewares: []httpc.MiddlewareFunc{
        httpc.RecoveryMiddleware(),
        httpc.LoggingMiddleware(log.Printf),
    },
    UserAgent:       "my-app/1.0",
    Headers:         map[string]string{"X-App": "my-app"},
    FollowRedirects: true,
    MaxRedirects:    10,
}

client, _ := httpc.New(cfg)

Next Steps