Documentation
¶
Overview ¶
Package middleware provides reusable HTTP middleware for production hardening.
Components include:
- ClientIPExtractor: Extracts real client IPs behind trusted reverse proxies
- RateLimiter: Global and per-key rate limiting with configurable key functions
- ConnLimiter: Concurrent connection limiting for WebSocket endpoints
- OriginChecker: Origin allowlist for WebSocket upgrade requests
- CORS: Origin-aware CORS header middleware
- Recovery: Panic recovery with structured logging
- RequestLogger: Structured HTTP request logging (includes request ID when available)
- RequestID: Request ID generation/propagation via X-Request-Id header
- BodyLimiter: Request body size limiting via http.MaxBytesReader
- HealthCheck: Health/readiness endpoint handler for load balancers and k8s probes
- Guard: Composable middleware chain
Helpers:
- ApplyDefaults: Sets sensible timeout defaults on http.Server (note: SSE endpoints require WriteTimeout = 0 separately to prevent the server from closing long-lived streaming connections)
All components are nil-safe: a nil component acts as a no-op passthrough. No application-specific imports — designed for embedding in any Go HTTP server.
Index ¶
- func ApplyDefaults(srv *http.Server)
- func CORS(checker *OriginChecker, opts ...CORSOption) func(http.Handler) http.Handler
- func ClientIP(r *http.Request) string
- func ContextWithRequestID(ctx context.Context, id string) context.Context
- func Recovery(next http.Handler) http.Handler
- func RequestIDFromContext(ctx context.Context) string
- func RequestLogger(skipPaths ...string) func(http.Handler) http.Handler
- func RequireContentType(accepted ...string) func(http.Handler) http.Handler
- func SetTrustedProxies(cidrs []string)
- type BodyLimiter
- type CORSOption
- type ClientIPExtractor
- type ConnLimiter
- type Guard
- type HealthCheck
- type HealthOption
- type OriginChecker
- type RateLimitConfig
- type RateLimiter
- type RequestID
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ApplyDefaults ¶ added in v0.0.7
ApplyDefaults sets sensible timeout defaults on an http.Server, only overwriting fields that are zero-valued.
Defaults:
- ReadTimeout: 10s
- WriteTimeout: 30s
- IdleTimeout: 120s
- ReadHeaderTimeout: 5s
For SSE or long-lived streaming endpoints, callers should either set WriteTimeout to a non-zero value before calling ApplyDefaults (it won't overwrite), or set WriteTimeout = 0 after calling this function to disable the write deadline for streaming responses.
Example ¶
ExampleApplyDefaults demonstrates setting sensible timeout defaults on http.Server. For SSE or WebSocket endpoints, set WriteTimeout = 0 after calling ApplyDefaults to prevent the server from killing long-lived connections.
package main
import (
"fmt"
"net/http"
mw "github.com/panyam/servicekit/middleware"
)
func main() {
srv := &http.Server{Addr: ":8080"}
mw.ApplyDefaults(srv)
// For SSE/WebSocket servers, override WriteTimeout
srv.WriteTimeout = 0
fmt.Printf("ReadTimeout: %v, WriteTimeout: %v, IdleTimeout: %v\n",
srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout)
}
Output: ReadTimeout: 10s, WriteTimeout: 0s, IdleTimeout: 2m0s
func CORS ¶
func CORS(checker *OriginChecker, opts ...CORSOption) func(http.Handler) http.Handler
CORS returns middleware that sets CORS headers based on an OriginChecker.
Behavior:
- If checker is nil (no allowlist): reflect any origin back
- If checker is set and the request Origin matches: reflect the origin back
- If checker is set and the request Origin doesn't match: no CORS headers (browser will block the response)
- OPTIONS preflight requests are handled and return 204
Use CORSOption values to customize allowed methods, headers, and exposed headers. Defaults: methods GET/POST/OPTIONS, headers Content-Type/Authorization, no exposed headers.
This replaces the naive "Access-Control-Allow-Origin: *" pattern with origin validation. The same OriginChecker used for WebSocket origin checks is reused here for consistency.
func ClientIP ¶
ClientIP extracts the real client IP using the default extractor. Configure trusted proxies via SetTrustedProxies.
func ContextWithRequestID ¶ added in v0.0.7
ContextWithRequestID returns a new context with the given request ID.
func Recovery ¶
Recovery returns middleware that recovers from panics in downstream handlers. On panic, it logs the error and stack trace, then returns 500 to the client. Without this, a single bad request can crash the entire server.
func RequestIDFromContext ¶ added in v0.0.7
RequestIDFromContext extracts the request ID from context, or "" if absent.
func RequestLogger ¶
RequestLogger logs HTTP requests with method, path, status, duration, and client IP. When a request ID is present in the context (set by the RequestID middleware), it is automatically included in the log output as "request_id". Skips logging for specified paths (e.g. /healthz for noisy liveness probes).
func RequireContentType ¶ added in v0.0.20
RequireContentType returns middleware that rejects POST, PUT, and PATCH requests whose Content-Type header does not match any of the accepted types (prefix match). GET, DELETE, OPTIONS, and HEAD requests are passed through since they typically have no body.
Returns 415 Unsupported Media Type for requests with a missing or mismatched Content-Type. This is a defense-in-depth measure against CSRF via cross-origin form submissions (browsers send forms as application/x-www-form-urlencoded without CORS preflight).
Example:
// Single type
mux.Handle("/api", middleware.RequireContentType("application/json")(handler))
// Multiple types
mux.Handle("/rpc", middleware.RequireContentType("application/json", "application/json-rpc")(handler))
func SetTrustedProxies ¶
func SetTrustedProxies(cidrs []string)
SetTrustedProxies configures the default (package-level) extractor. This is a convenience for simple use cases; prefer NewClientIPExtractor for instance isolation.
Types ¶
type BodyLimiter ¶ added in v0.0.7
type BodyLimiter struct {
// contains filtered or unexported fields
}
BodyLimiter limits the size of incoming request bodies using http.MaxBytesReader. When the handler reads past the limit, MaxBytesReader returns an error and the request fails.
Nil-safe: a nil BodyLimiter passes requests through unchanged.
func NewBodyLimiter ¶ added in v0.0.7
func NewBodyLimiter(maxBytes int64) *BodyLimiter
NewBodyLimiter creates a limiter with the given max body size in bytes. Returns nil if maxBytes <= 0 (caller should skip the middleware).
func (*BodyLimiter) Middleware ¶ added in v0.0.7
func (b *BodyLimiter) Middleware(next http.Handler) http.Handler
Middleware returns an HTTP middleware that limits request body size. On a nil receiver, returns the handler unchanged.
type CORSOption ¶ added in v0.0.26
type CORSOption func(*corsConfig)
CORSOption configures the CORS middleware.
func CORSAllowHeaders ¶ added in v0.0.26
func CORSAllowHeaders(headers ...string) CORSOption
CORSAllowHeaders overrides the default allowed headers (Content-Type, Authorization).
func CORSAllowMethods ¶ added in v0.0.26
func CORSAllowMethods(methods ...string) CORSOption
CORSAllowMethods overrides the default allowed methods (GET, POST, OPTIONS).
func CORSExposeHeaders ¶ added in v0.0.26
func CORSExposeHeaders(headers ...string) CORSOption
CORSExposeHeaders sets the Access-Control-Expose-Headers value. By default no headers are exposed.
type ClientIPExtractor ¶
type ClientIPExtractor struct {
// contains filtered or unexported fields
}
ClientIPExtractor extracts the real client IP from requests, honoring X-Forwarded-For and X-Real-IP headers only when the direct connection comes from a trusted proxy CIDR.
Usage:
// Trust Caddy on localhost and Docker bridge network
extractor := NewClientIPExtractor([]string{"127.0.0.1/32", "172.17.0.0/16", "::1/128"})
ip := extractor.ClientIP(r)
// Trust all proxies (suitable for single-proxy deployments)
extractor := NewClientIPExtractor(nil)
func NewClientIPExtractor ¶
func NewClientIPExtractor(cidrs []string) *ClientIPExtractor
NewClientIPExtractor creates an extractor with the given trusted proxy CIDRs. When cidrs is nil/empty, all proxies are trusted (backwards-compatible default).
func (*ClientIPExtractor) ExtractClientIP ¶
func (e *ClientIPExtractor) ExtractClientIP(r *http.Request) string
ExtractClientIP extracts the real client IP from the request.
If trusted proxies are configured, X-Forwarded-For is only honored when the direct connection (RemoteAddr) comes from a trusted proxy CIDR. Otherwise, the direct RemoteAddr is used.
If no trusted proxies are configured (default), X-Forwarded-For is always trusted (backwards-compatible for deployments behind a proxy).
type ConnLimiter ¶
type ConnLimiter struct {
// contains filtered or unexported fields
}
ConnLimiter limits the number of concurrent active requests to a handler. Designed for WebSocket endpoints where each connection holds resources for the lifetime of the session.
When the limit is reached, new requests receive 503 Service Unavailable. The counter decrements when the request handler returns (connection closes).
func NewConnLimiter ¶
func NewConnLimiter(max int64) *ConnLimiter
NewConnLimiter creates a limiter with the given max concurrent connections. Pass 0 for unlimited (returns nil — caller should skip the middleware).
func (*ConnLimiter) Active ¶
func (c *ConnLimiter) Active() int64
Active returns the current number of active connections.
func (*ConnLimiter) Middleware ¶
func (c *ConnLimiter) Middleware(next http.Handler) http.Handler
Middleware returns an HTTP middleware that enforces the connection limit.
type Guard ¶
type Guard struct {
// contains filtered or unexported fields
}
Guard composes multiple middleware into a single wrapper. Middleware are applied in the order they are added via Use — the first middleware added is the outermost (runs first on request, last on response).
Usage:
g := &Guard{}
g.Use(originChecker.Middleware, rateLimiter.Middleware(nil), auth.Middleware, connLimiter.Middleware)
http.Handle("/ws", g.Wrap(wsHandler))
Example ¶
ExampleGuard demonstrates composing middleware into a chain using Guard. Middleware are applied in Use() order — the first added is outermost (runs first on request, last on response). Nil middleware are silently skipped.
package main
import (
"fmt"
"net/http"
mw "github.com/panyam/servicekit/middleware"
)
func main() {
// Create middleware components
rl := mw.NewRateLimiter(mw.RateLimitConfig{
GlobalPerSec: 100,
PerKeyPerSec: 10,
})
oc := mw.NewOriginChecker([]string{
"https://example.com",
"https://*.example.com",
})
// Compose into a Guard chain
g := &mw.Guard{}
g.Use(
oc.Middleware, // Check origin first
rl.Middleware(nil), // Then rate limit (nil key = use ClientIP)
mw.Recovery, // Catch panics
)
// Wrap your handler
handler := g.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
http.Handle("/api", handler)
_ = handler
fmt.Println("Guard chain configured")
}
Output: Guard chain configured
type HealthCheck ¶ added in v0.0.7
type HealthCheck struct {
// contains filtered or unexported fields
}
HealthCheck is an HTTP handler that serves health/readiness endpoints for load balancers and Kubernetes probes. It implements http.Handler and should be mounted directly on a mux, not used as middleware.
Nil-safe: a nil *HealthCheck returns 404.
func NewHealthCheck ¶ added in v0.0.7
func NewHealthCheck(opts ...HealthOption) *HealthCheck
NewHealthCheck creates a health check handler with the given options. Default path is "/healthz".
func (*HealthCheck) Path ¶ added in v0.0.7
func (h *HealthCheck) Path() string
Path returns the configured endpoint path for mux registration.
func (*HealthCheck) ServeHTTP ¶ added in v0.0.7
func (h *HealthCheck) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler. On a nil receiver, returns 404.
type HealthOption ¶ added in v0.0.7
type HealthOption func(*HealthCheck)
HealthOption configures the health check handler.
func WithPath ¶ added in v0.0.7
func WithPath(path string) HealthOption
WithPath sets the health endpoint path (default "/healthz").
func WithReadyFunc ¶ added in v0.0.7
func WithReadyFunc(fn func() bool) HealthOption
WithReadyFunc sets an optional readiness callback. When set and returning false, the endpoint returns 503 with {"status":"not ready"}.
type OriginChecker ¶
type OriginChecker struct {
// contains filtered or unexported fields
}
OriginChecker rejects requests whose Origin header doesn't match an allowlist. Designed for WebSocket endpoints where CORS headers alone don't prevent cross-origin connections.
Matching rules:
- Exact match on scheme+host (port-insensitive for 80/443)
- Wildcard subdomain: "*.example.com" matches "foo.example.com"
- "localhost" matches any localhost origin regardless of port
- Empty allowlist = allow all (no-op)
- Missing Origin header = blocked (unless allowlist is empty)
func NewLocalhostOriginChecker ¶ added in v0.0.18
func NewLocalhostOriginChecker() *OriginChecker
NewLocalhostOriginChecker creates an OriginChecker that only allows localhost origins (localhost, 127.0.0.1, ::1). This is the secure default for local development servers that don't have an explicit allowlist.
Unlike NewOriginChecker(nil) which returns nil (allow-all), this returns a restrictive checker that blocks remote origins.
func NewOriginChecker ¶
func NewOriginChecker(origins []string) *OriginChecker
NewOriginChecker creates a checker from a list of allowed origins. Accepts formats: "https://example.com", "*.example.com", "localhost", "*". Returns nil if the list is empty or contains "*" (allow all origins).
Example ¶
ExampleNewOriginChecker demonstrates WebSocket origin validation with exact matches, wildcard subdomains, and localhost support. The checker is typically used via Guard middleware, but Check() can be called directly.
package main
import (
"fmt"
mw "github.com/panyam/servicekit/middleware"
)
func main() {
oc := mw.NewOriginChecker([]string{
"https://example.com", // exact match
"*.example.com", // wildcard subdomain
"localhost", // any localhost port
})
fmt.Println(oc.Check("https://example.com")) // true
fmt.Println(oc.Check("https://app.example.com")) // true
fmt.Println(oc.Check("http://localhost:3000")) // true
fmt.Println(oc.Check("https://evil.com")) // false
}
Output: true true true false
func (*OriginChecker) Check ¶
func (c *OriginChecker) Check(origin string) bool
Check returns true if the origin is allowed.
func (*OriginChecker) CheckRequest ¶ added in v0.0.18
func (c *OriginChecker) CheckRequest(r *http.Request) bool
CheckRequest validates an HTTP request's origin using both Origin and Host headers. This implements the full DNS rebinding protection pattern:
- Origin header present → delegates to Check(origin)
- Origin absent, Host is localhost → allow (local dev tools like curl)
- Origin absent, Host is remote → reject (potential DNS rebinding)
- No Origin or Host info → allow (same-origin or non-browser client)
Nil-safe: calling on a nil *OriginChecker allows all requests.
func (*OriginChecker) Middleware ¶
func (c *OriginChecker) Middleware(next http.Handler) http.Handler
Middleware returns an HTTP middleware that rejects disallowed origins. Only applies to WebSocket upgrade requests (Connection: Upgrade). Non-upgrade requests pass through unchanged.
type RateLimitConfig ¶
type RateLimitConfig struct {
GlobalPerSec float64 // max requests/sec globally (0 = unlimited)
PerKeyPerSec float64 // max requests/sec per key (0 = unlimited)
PerKeyBurst int // burst allowance per key
KeyLimiterTTL time.Duration // cleanup interval for stale per-key limiters
}
RateLimitConfig controls rate limiting.
func DefaultRateLimitConfig ¶
func DefaultRateLimitConfig() RateLimitConfig
DefaultRateLimitConfig returns sensible defaults.
type RateLimiter ¶
type RateLimiter struct {
Config RateLimitConfig
// OnRejected is called when a request is rate-limited.
// The key argument is the rate limit key (e.g., IP address, subject ID).
OnRejected func(key string)
// contains filtered or unexported fields
}
RateLimiter enforces global and per-key rate limits.
func NewRateLimiter ¶
func NewRateLimiter(cfg RateLimitConfig) *RateLimiter
NewRateLimiter creates a rate limiter. Returns nil if both limits are 0.
Example ¶
ExampleNewRateLimiter demonstrates per-key rate limiting with a custom key function. The default key is client IP; override it to rate limit per user, per API key, or per any request attribute.
package main
import (
"fmt"
"net/http"
mw "github.com/panyam/servicekit/middleware"
)
func main() {
rl := mw.NewRateLimiter(mw.RateLimitConfig{
GlobalPerSec: 100,
PerKeyPerSec: 5,
PerKeyBurst: 3,
})
// Rate limit by API key header instead of IP
apiKeyFunc := func(r *http.Request) string {
return r.Header.Get("X-API-Key")
}
handler := rl.Middleware(apiKeyFunc)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
_ = handler
fmt.Println("Rate limiter configured")
}
Output: Rate limiter configured
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow(key string) bool
Allow checks both global and per-key rate limits. Returns false if rejected.
func (*RateLimiter) Middleware ¶
func (rl *RateLimiter) Middleware(keyFunc func(*http.Request) string) func(http.Handler) http.Handler
Middleware returns an HTTP middleware that enforces rate limits. keyFunc extracts the rate limit key from the request. If nil, defaults to ClientIP.
type RequestID ¶ added in v0.0.7
type RequestID struct{}
RequestID generates or propagates request IDs via the X-Request-Id header. If the incoming request has an X-Request-Id header, it is preserved. Otherwise, a new 32-character hex string (16 random bytes) is generated. The ID is stored in the request context and set on the response header.
func NewRequestID ¶ added in v0.0.7
func NewRequestID() *RequestID
NewRequestID creates a RequestID middleware instance.