muxhandlers

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: May 16, 2026 License: MIT Imports: 30 Imported by: 0

README

muxhandlers

HTTP middleware handlers for the mux router.

Installation

go get github.com/vitalvas/kasper/muxhandlers

CORS Middleware

CORSMiddleware implements the full CORS protocol. It validates the Origin header, handles preflight OPTIONS requests, and sets the appropriate response headers.

CORSConfig
Field Type Description
AllowedOrigins []string Exact origins, "*" for wildcard, or subdomain patterns
AllowOriginFunc func(string) bool Optional dynamic origin check (receives raw origin)
AllowedMethods []string Override methods; empty = auto-discover from router
AllowedHeaders []string Preflight allowed headers; empty = reflect request
ExposeHeaders []string Headers exposed to client code
AllowCredentials bool Send Access-Control-Allow-Credentials: true
MaxAge int Preflight cache seconds; 0 = omit, negative = "0"
OptionsStatusCode int Preflight HTTP status; 0 = 204 No Content
OptionsPassthrough bool Forward preflight to next handler after setting headers
AllowPrivateNetwork bool Respond to private network access requests
CORS Usage
r := mux.NewRouter()

r.HandleFunc("/users", listUsers).Methods(http.MethodGet)
r.HandleFunc("/users", createUser).Methods(http.MethodPost)

mw, err := muxhandlers.CORSMiddleware(r, muxhandlers.CORSConfig{
    AllowedOrigins:   []string{"https://example.com"},
    AllowCredentials: true,
    AllowedHeaders:   []string{"Content-Type", "Authorization"},
    ExposeHeaders:    []string{"X-Request-Id"},
    MaxAge:           3600,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Basic Auth Middleware

BasicAuthMiddleware implements HTTP Basic Authentication per RFC 7617. Credentials can be validated via a dynamic callback (ValidateFunc) or a static map (Credentials). When both are set, ValidateFunc takes priority. Static credential comparison uses crypto/subtle.ConstantTimeCompare to prevent timing attacks.

BasicAuthConfig
Field Type Description
Realm string Authentication realm for WWW-Authenticate header
ValidateFunc func(string, string) bool Dynamic credential validation callback
Credentials map[string]string Static username-to-password map
BasicAuth Usage with ValidateFunc
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.BasicAuthMiddleware(muxhandlers.BasicAuthConfig{
    Realm: "My App",
    ValidateFunc: func(username, password string) bool {
        return username == "admin" && password == "secret"
    },
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)
BasicAuth Usage with Credentials
mw, err := muxhandlers.BasicAuthMiddleware(muxhandlers.BasicAuthConfig{
    Credentials: map[string]string{
        "admin": "secret",
        "user":  "password",
    },
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Bearer Auth Middleware

BearerAuthMiddleware implements HTTP Bearer Token Authentication per RFC 6750. It extracts the token from the Authorization header and validates it using a user-provided function. When the token is missing, malformed, or invalid, the middleware responds with 401 Unauthorized and a WWW-Authenticate: Bearer header per RFC 6750 Section 3.

BearerAuthConfig
Field Type Description
Realm string Authentication realm for WWW-Authenticate header; defaults to "Restricted"
ValidateFunc func(*http.Request, string) bool Token validation callback; receives request and raw token
BearerAuth Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.BearerAuthMiddleware(muxhandlers.BearerAuthConfig{
    Realm: "My API",
    ValidateFunc: func(r *http.Request, token string) bool {
        return token == expectedToken
    },
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Proxy Headers Middleware

ProxyHeadersMiddleware populates request fields from reverse proxy headers when the request originates from a trusted proxy. A trusted proxy list (IPs and CIDRs) restricts which peers are allowed to set these headers, preventing spoofing from untrusted clients. When TrustedProxies is empty, DefaultTrustedProxies (RFC 1918, RFC 4193, and loopback ranges) is used.

Supported Headers
Field Headers (priority order)
r.RemoteAddr X-Forwarded-For > X-Real-IP > Forwarded: for=*
r.URL.Scheme X-Forwarded-Proto > X-Forwarded-Scheme*
r.Host X-Forwarded-Host > Forwarded: host=*
X-Forwarded-By header Forwarded: by=*

*Requires EnableForwarded: true (RFC 7239).

DefaultTrustedProxies
Range Description
127.0.0.0/8 IPv4 loopback (RFC 1122)
10.0.0.0/8 Class A private (RFC 1918)
172.16.0.0/12 Class B private (RFC 1918)
192.168.0.0/16 Class C private (RFC 1918)
100.64.0.0/10 CGNAT shared address space (RFC 6598)
::1/128 IPv6 loopback (RFC 4291)
fc00::/7 IPv6 unique local (RFC 4193)
ProxyHeadersConfig
Field Type Description
TrustedProxies []string IP/CIDR of trusted proxies; empty = defaults
EnableForwarded bool Parse RFC 7239 Forwarded header as fallback
ProxyHeaders Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.ProxyHeadersMiddleware(muxhandlers.ProxyHeadersConfig{
    TrustedProxies: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Recovery Middleware

RecoveryMiddleware recovers from panics in downstream handlers, returns 500 Internal Server Error to the client, and optionally invokes a custom log callback with the request and recovered value.

RecoveryConfig
Field Type Description
LogFunc func(*http.Request, any) Optional callback; nil = no logging
Recovery Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

r.Use(muxhandlers.RecoveryMiddleware(muxhandlers.RecoveryConfig{
    LogFunc: func(r *http.Request, err any) {
        log.Printf("panic: %v %s", err, r.URL.Path)
    },
}))

Request ID Middleware

RequestIDMiddleware generates or propagates a unique request identifier. The ID is set on the request header, the response header, and the request context. Downstream handlers can retrieve it with RequestIDFromContext. By default it generates UUID v4 values using github.com/google/uuid. Use GenerateUUIDv7 for time-ordered IDs (RFC 9562). The GenerateFunc receives the current request, allowing ID generation based on request context.

RequestIDConfig
Field Type Description
HeaderName string Header name; defaults to "X-Request-ID"
GenerateFunc func(*http.Request) string Custom ID generator; defaults to UUID v4
TrustIncoming bool Reuse existing header from the incoming request
Built-in Generators
Function Description
GenerateUUIDv4 Random UUID v4 (default)
GenerateUUIDv7 Time-ordered UUID v7 (RFC 9562)
RequestID Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

r.Use(muxhandlers.RequestIDMiddleware(muxhandlers.RequestIDConfig{
    TrustIncoming: true,
}))
RequestID Usage with UUID v7
r.Use(muxhandlers.RequestIDMiddleware(muxhandlers.RequestIDConfig{
    GenerateFunc: muxhandlers.GenerateUUIDv7,
}))
Reading the ID from context
func handler(w http.ResponseWriter, r *http.Request) {
    id := muxhandlers.RequestIDFromContext(r.Context())
    log.Printf("request %s", id)
}

Request Size Limit Middleware

RequestSizeLimitMiddleware limits the size of incoming request bodies. It wraps r.Body with http.MaxBytesReader, which returns 413 Request Entity Too Large when the limit is exceeded.

RequestSizeLimitConfig
Field Type Description
MaxBytes int64 Maximum allowed body size in bytes; must be > 0
RequestSizeLimit Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/upload", handleUpload).Methods(http.MethodPost)

mw, err := muxhandlers.RequestSizeLimitMiddleware(muxhandlers.RequestSizeLimitConfig{
    MaxBytes: 1 << 20, // 1 MiB
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Timeout Middleware

TimeoutMiddleware limits handler execution time by wrapping the handler with http.TimeoutHandler. It returns 503 Service Unavailable when the handler does not complete within the configured duration.

TimeoutConfig
Field Type Description
Duration time.Duration Maximum handler execution time; must be > 0
Message string Custom timeout body; empty = stdlib default
Timeout Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.TimeoutMiddleware(muxhandlers.TimeoutConfig{
    Duration: 30 * time.Second,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Compression Middleware

CompressionMiddleware compresses response bodies using gzip or deflate when the client advertises support via the Accept-Encoding header. Gzip is preferred over deflate when both are accepted. Quality values (q=) are respected for encoding selection. It uses sync.Pool instances to reuse writers for performance. Compression is skipped for inherently compressed content types (images, video, audio, archives) and when a Content-Encoding is already set.

CompressionConfig
Field Type Description
Level int Compression level; 0 = default; [HuffmanOnly, BestCompression]
MinLength int Minimum body bytes before compressing; 0 = always
Compression Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.CompressionMiddleware(muxhandlers.CompressionConfig{
    Level:     gzip.BestSpeed,
    MinLength: 1024,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Security Headers Middleware

SecurityHeadersMiddleware sets common security response headers with sensible defaults. By default it sets X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Referrer-Policy: strict-origin-when-cross-origin. HSTS, CSP, Permissions-Policy, and Cross-Origin-Opener-Policy are opt-in.

SecurityHeadersConfig
Field Type Description
DisableContentTypeNosniff bool Disable nosniff; enabled by default
FrameOption string "DENY" (default), "SAMEORIGIN", or empty
ReferrerPolicy string Defaults to "strict-origin-when-cross-origin"
HSTSMaxAge int HSTS max-age in seconds; 0 = skip
HSTSIncludeSubDomains bool Append includeSubDomains; requires HSTSMaxAge
HSTSPreload bool Append preload; requires HSTSMaxAge
CrossOriginOpenerPolicy string COOP value; empty = skip
ContentSecurityPolicy string CSP value; empty = skip
PermissionsPolicy string Permissions-Policy value; empty = skip
SecurityHeaders Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.SecurityHeadersMiddleware(muxhandlers.SecurityHeadersConfig{
    HSTSMaxAge:            63072000,
    HSTSIncludeSubDomains: true,
    HSTSPreload:           true,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Method Override Middleware

MethodOverrideMiddleware allows clients to override the HTTP method via a configurable header. By default only POST requests are eligible for override; use OriginalMethods to allow other methods. The first non-empty header value from HeaderNames is uppercased and checked against the allowed set. When allowed, r.Method is updated and the header is removed from the request.

MethodOverrideConfig
Field Type Description
HeaderNames []string Header names checked in order; first non-empty value wins; nil = X-HTTP-Method-Override, X-Method-Override, X-HTTP-Method
OriginalMethods []string Methods eligible for override; nil = POST
AllowedMethods []string Allowed override methods; nil = PUT, PATCH, DELETE, HEAD, OPTIONS
MethodOverride Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", updateUser).Methods(http.MethodPut)

mw, err := muxhandlers.MethodOverrideMiddleware(muxhandlers.MethodOverrideConfig{})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Content-Type Check Middleware

ContentTypeCheckMiddleware validates that requests carry a matching Content-Type header. Matching is case-insensitive and ignores parameters such as charset (e.g. "application/json" matches "application/json; charset=utf-8"). It returns 415 Unsupported Media Type when the Content-Type is missing or does not match any of the allowed types. By default it checks POST, PUT, and PATCH requests.

ContentTypeCheckConfig
Field Type Description
AllowedTypes []string Acceptable Content-Type values; case-insensitive, ignores params
Methods []string HTTP methods that require validation; nil = POST, PUT, PATCH
ContentTypeCheck Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", createUser).Methods(http.MethodPost)

mw, err := muxhandlers.ContentTypeCheckMiddleware(muxhandlers.ContentTypeCheckConfig{
    AllowedTypes: []string{"application/json"},
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Server Middleware

ServerMiddleware sets server identification response headers. It sets X-Server-Hostname with the machine hostname, resolved once at factory time via os.Hostname. Use the Hostname field to provide a static value instead.

ServerConfig
Field Type Description
Hostname string Static hostname value; takes priority over HostnameEnv
HostnameEnv []string Environment variable names checked in order (e.g. ["POD_NAME", "HOSTNAME"]); first non-empty wins; fallback = os.Hostname()
Server Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.ServerMiddleware(muxhandlers.ServerConfig{})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Cache-Control Middleware

CacheControlMiddleware sets Cache-Control and Expires response headers based on the response Content-Type. Rules are evaluated in order; the first rule whose ContentType prefix matches wins. If no rule matches and DefaultValue/DefaultExpires is non-empty, it is used. When the handler already sets a Cache-Control or Expires header, the middleware does not overwrite the respective header. Matching is case-insensitive.

CacheControlRule
Field Type Description
ContentType string Content type prefix to match (e.g. "image/", "application/json")
Value string Cache-Control header value to set when this rule matches
Expires time.Duration Offset from current time for the Expires header; 0 = epoch (already expired); negative = no header
CacheControlConfig
Field Type Description
Rules []CacheControlRule Ordered list of rules; first match wins; at least one required
DefaultValue string Cache-Control value for unmatched types; empty = no header
DefaultExpires time.Duration Default Expires offset for unmatched types; negative = no header
CacheControl Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.CacheControlMiddleware(muxhandlers.CacheControlConfig{
    Rules: []muxhandlers.CacheControlRule{
        {ContentType: "image/", Value: "public, max-age=86400", Expires: 24 * time.Hour},
        {ContentType: "application/json", Value: "no-cache", Expires: 0},
    },
    DefaultValue:   "no-store",
    DefaultExpires: 0,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Static Files Handler

StaticFilesHandler serves static files from any fs.FS implementation (os.DirFS, embed.FS, fstest.MapFS, etc.) using http.FileServerFS. It is not middleware — it returns an http.Handler that serves files directly. Directory listing is disabled by default; when a directory has no index.html, a 404 is returned instead of a file listing. When SPAFallback is enabled, requests for non-existent paths serve the root index.html, allowing client-side routers to handle routing.

StaticFilesConfig
Field Type Description
FS fs.FS File system to serve files from; required
EnableDirectoryListing bool Show directory contents when no index.html is present; false by default
SPAFallback bool Serve root index.html for non-existent paths; requires index.html at FS root
EnableETag bool Precompute strong ETags at init; handles If-None-Match (304); designed for embed.FS
PathPrefix string URL path prefix to strip internally; replaces http.StripPrefix
Aliases map[string]string Maps URL paths (relative to PathPrefix) to file paths in the FS; targets validated at init; ETag support applies
StaticFiles Usage
r := mux.NewRouter()

handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS: os.DirFS("./public"),
})
if err != nil {
    log.Fatal(err)
}

r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", handler))
StaticFiles Usage with SPA
r := mux.NewRouter()

// API routes first
r.PathPrefix("/api/").Handler(apiRouter)

// SPA handler last — catches all unmatched routes
handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS:          os.DirFS("./public"),
    SPAFallback: true,
})
if err != nil {
    log.Fatal(err)
}

r.PathPrefix("/").Handler(handler)
StaticFiles Usage with embed.FS
//go:embed static
var staticFS embed.FS

handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS: staticFS,
})
if err != nil {
    log.Fatal(err)
}

r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", handler))
StaticFiles Usage with ETag
//go:embed static
var staticFS embed.FS

handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS:         staticFS,
    EnableETag: true,
})
if err != nil {
    log.Fatal(err)
}

r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", handler))
StaticFiles Usage with PathPrefix and Aliases
handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS:         staticFS,
    PathPrefix: "/ui",
    EnableETag: true,
    Aliases: map[string]string{
        "/policy-builder/":    "policy-builder.html",
        "/policy-playground/": "policy-playground.html",
    },
})
if err != nil {
    log.Fatal(err)
}

r.PathPrefix("/ui/").Handler(handler)
// /ui/policy-builder/    -> policy-builder.html
// /ui/policy-playground/ -> policy-playground.html
// /ui/style.css          -> style.css

Profiler Handler

RegisterProfiler registers the standard net/http/pprof and expvar endpoints on the given router. It is not middleware — it registers routes directly. Endpoints use the standard /debug/pprof/ and /debug/vars paths. Mount with any prefix using Route:

Registered Endpoints
Suffix Description
/debug/pprof/ Index page with links to all profiles
/debug/pprof/cmdline Running program command line
/debug/pprof/profile CPU profile (supports ?seconds=N)
/debug/pprof/symbol Symbol lookup
/debug/pprof/trace Execution trace (supports ?seconds=N)
/debug/vars Exported variables via expvar package

Named profiles (allocs, block, goroutine, heap, mutex, threadcreate) are served by the index handler.

Profiler Usage
r := mux.NewRouter()

RegisterProfiler(r)
// serves /debug/pprof/, /debug/vars, etc.
Profiler Usage with custom prefix
r := mux.NewRouter()

r.Route("/_internal", muxhandlers.RegisterProfiler)
// serves /_internal/debug/pprof/, /_internal/debug/vars, etc.

Sunset Middleware

SunsetMiddleware sets the Sunset response header per RFC 8594 to indicate that a resource is expected to become unresponsive at a specific point in time. Optionally sets the Deprecation header and a Link header with rel="sunset" pointing to migration documentation.

SunsetConfig
Field Type Description
Sunset time.Time When the resource becomes unresponsive; required; serialized as HTTP-date
Deprecation time.Time When the resource was deprecated; zero = omit
Link string URI to deprecation/migration docs; empty = omit; added as Link header with rel="sunset"
Sunset Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.SunsetMiddleware(muxhandlers.SunsetConfig{
    Sunset:      time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
    Deprecation: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
    Link:        "https://example.com/docs/migration",
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Idempotency Middleware

IdempotencyMiddleware caches responses keyed by the Idempotency-Key header per draft-ietf-httpapi-idempotency-key-header. Duplicate requests with the same key replay the cached response without invoking the handler. The middleware requires an IdempotencyStore implementation for persistence.

IdempotencyStore Interface
type IdempotencyStore interface {
    Get(ctx context.Context, key string) ([]byte, bool)
    Set(ctx context.Context, key string, value []byte, ttl time.Duration)
}
IdempotencyLocker Interface
type IdempotencyLocker interface {
    Lock(ctx context.Context, key string) bool
    Unlock(ctx context.Context, key string)
}
IdempotencyConfig
Field Type Description
Store IdempotencyStore Backing store for cached responses; required
HeaderName string Header name; defaults to "Idempotency-Key"
TTL time.Duration Cache TTL; defaults to 24 hours; zero = no expiry
Methods []string HTTP methods that require idempotency; nil = POST
EnforceKey bool Return 400 if header is missing on matched methods
CacheableStatusCodes []int Status codes to cache; nil = cache all; e.g. []int{200, 201}
CacheKeyFunc func(*http.Request, string) string Custom cache key builder; receives request and raw key; nil = default scoping by method + path + key
ValidateKeyFunc func(*http.Request, string) bool Key format validator; return false to reject with 400; nil = no validation
KeyMaxLength int Maximum key length; 0 = default (64); -1 = no limit
CanCache func(*http.Request) bool Pre-check before cache lookup/storage; return false to skip caching; nil = all matched requests are eligible
OnCacheHit func(*http.Request, string) Called on cache hit with request and raw key; use for observability; nil = no callback
OnCacheMiss func(*http.Request, string) Called on cache miss with request and raw key; use for observability; nil = no callback
Locker IdempotencyLocker Optional distributed lock for in-flight requests; returns 409 Conflict when lock cannot be acquired; nil = no locking
FingerprintFunc func(*http.Request) string Computes a request fingerprint; mismatched fingerprint on cache hit returns 422 Unprocessable Entity; nil = no fingerprint check
OnConflict func(*http.Request, string) Called on 409 Conflict (lock failure); use for observability; nil = no callback
OnFingerprintMismatch func(*http.Request, string) Called on 422 (fingerprint mismatch); use for observability; nil = no callback
RetryAfter time.Duration Duration for Retry-After header (as whole seconds) on 409 Conflict responses; 0 = no header
ReplayedHeaderName string Response header set to "true" on replayed responses; empty = no header; e.g. "X-Idempotency-Replayed"
ErrorHandler func(http.ResponseWriter, *http.Request, int) Custom error writer for 400/409/422 responses; nil = plain-text http.Error
OnStore func(*http.Request, string, int) Called when a response is stored in cache with request, key, and status code; nil = no callback
ResponseHeadersFunc func(http.Header, *http.Request, bool) Called before writing any response; replayed param is true for cached replays; nil = no callback
MaxCacheBodySize int64 Maximum response body size in bytes to cache; larger responses are served but not stored; 0 = no limit
Route Metadata

Use the IdempotencySkipMetadataKey constant to skip idempotency processing for specific routes via route metadata:

r.HandleFunc("/health", handler).
    Methods(http.MethodPost).
    Metadata(muxhandlers.IdempotencySkipMetadataKey, true)
Idempotency Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/payments", createPayment).Methods(http.MethodPost)

mw, err := muxhandlers.IdempotencyMiddleware(muxhandlers.IdempotencyConfig{
    Store: redisStore, // implements IdempotencyStore
    TTL:   1 * time.Hour,
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)
Idempotency Usage with EnforceKey
mw, err := muxhandlers.IdempotencyMiddleware(muxhandlers.IdempotencyConfig{
    Store:      redisStore,
    EnforceKey: true, // 400 if Idempotency-Key header is missing
})
Idempotency Usage with CacheKeyFunc (per-user scoping)
mw, err := muxhandlers.IdempotencyMiddleware(muxhandlers.IdempotencyConfig{
    Store: redisStore,
    CacheKeyFunc: func(r *http.Request, key string) string {
        userID := r.Header.Get("X-User-ID")
        return userID + ":" + r.Method + ":" + r.URL.Path + ":" + key
    },
})

Content Negotiation Middleware

ContentNegotiationMiddleware performs proactive content negotiation per RFC 9110 Section 12.5.1. It parses the Accept header with quality values, selects the best matching type from the offered list, and stores the result in the request context. Returns 406 Not Acceptable when no match is found.

ContentNegotiationConfig
Field Type Description
Offered []string Media types the server can produce, in preference order; empty = accept all
ContentNegotiation Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", handler).Methods(http.MethodGet)

r.Use(muxhandlers.ContentNegotiationMiddleware(muxhandlers.ContentNegotiationConfig{
    Offered: []string{"application/json", "application/xml"},
}))
Reading the Negotiated Type
func handler(w http.ResponseWriter, r *http.Request) {
    switch muxhandlers.NegotiatedType(r) {
    case "application/json":
        mux.ResponseJSON(w, http.StatusOK, data)
    case "application/xml":
        mux.ResponseXML(w, http.StatusOK, data)
    }
}
Negotiation Rules
Accept Header Behavior
absent or empty First offered type is selected
application/json Exact match
text/* Matches any text/ subtype
*/* Matches any type; first offered wins
application/json;q=0.5, text/html;q=0.9 Higher quality wins
application/json;q=0 Explicitly excluded
text/csv (not offered) 406 Not Acceptable

Problem Details

WriteProblemDetails writes an RFC 9457 Problem Details JSON response with Content-Type application/problem+json.

ProblemDetails
Field Type Description
Type string URI identifying the problem type; defaults to "about:blank"
Title string Short human-readable summary
Status int HTTP status code
Detail string Human-readable explanation specific to this occurrence
Instance string URI identifying the specific occurrence
Extensions map[string]any Additional members merged into the top-level JSON object
ProblemDetails Usage
func handler(w http.ResponseWriter, r *http.Request) {
    user, err := db.GetUser(id)
    if err != nil {
        muxhandlers.WriteProblemDetails(w, muxhandlers.ProblemDetails{
            Type:   "https://example.com/errors/not-found",
            Title:  "Resource not found",
            Status: http.StatusNotFound,
            Detail: fmt.Sprintf("User with ID %s was not found", id),
        })
        return
    }
}
ProblemDetails with Extensions
muxhandlers.WriteProblemDetails(w, muxhandlers.ProblemDetails{
    Type:   "https://example.com/errors/validation",
    Title:  "Validation Error",
    Status: http.StatusUnprocessableEntity,
    Detail: "One or more fields are invalid",
    Extensions: map[string]any{
        "errors": []map[string]string{
            {"field": "email", "message": "invalid format"},
        },
    },
})
Quick Error Response

NewProblemDetails creates a ProblemDetails with the status code and the standard status text as title:

muxhandlers.WriteProblemDetails(w, muxhandlers.NewProblemDetails(http.StatusForbidden))

Early Hints Middleware

EarlyHintsMiddleware sends a 103 Early Hints informational response per RFC 8297 before the final response. This allows clients to begin preloading resources (stylesheets, scripts, fonts) while the server is still processing the request.

EarlyHintsConfig
Field Type Description
Links []string Static Link header values per RFC 8288
LinksFunc func(*http.Request) []string Per-request dynamic link computation; results are sent alongside static Links

Either Links or LinksFunc (or both) must be set.

EarlyHints Usage
r := mux.NewRouter()

r.HandleFunc("/", pageHandler).Methods(http.MethodGet)

mw, err := muxhandlers.EarlyHintsMiddleware(muxhandlers.EarlyHintsConfig{
    Links: []string{
        `</style.css>; rel=preload; as=style`,
        `</app.js>; rel=preload; as=script`,
        `</font.woff2>; rel=preload; as=font; crossorigin`,
    },
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)
EarlyHints Usage with embed.FS
//go:embed static
var staticFS embed.FS

// buildLinks walks the embedded FS and returns Link headers for
// known asset types.
func buildLinks() []string {
    asType := map[string]string{
        ".css":   "style",
        ".js":    "script",
        ".woff2": "font",
    }

    var links []string
    fs.WalkDir(staticFS, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil || d.IsDir() {
            return err
        }
        ext := filepath.Ext(path)
        as, ok := asType[ext]
        if !ok {
            return nil
        }
        link := fmt.Sprintf("<%s>; rel=preload; as=%s", "/"+path, as)
        if as == "font" {
            link += "; crossorigin"
        }
        links = append(links, link)
        return nil
    })
    return links
}

mw, err := muxhandlers.EarlyHintsMiddleware(muxhandlers.EarlyHintsConfig{
    Links: buildLinks(),
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Accept-Patch Middleware

AcceptPatchMiddleware handles OPTIONS requests by responding with Allow and Accept-Patch headers per RFC 5789. The Allow header is auto-discovered from the router's registered methods for the matched path. Non-OPTIONS requests pass through unchanged.

AcceptPatchConfig
Field Type Description
AcceptPatchTypes []string Content-Type values for Accept-Patch header; nil = application/json, application/merge-patch+json, application/json-patch+json
StatusCode int HTTP status for OPTIONS responses; 0 = 204 No Content
AcceptPatch Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users/{id}", getUser).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/{id}", updateUser).Methods(http.MethodPatch)
r.HandleFunc("/api/v1/users/{id}", deleteUser).Methods(http.MethodDelete)

mw := muxhandlers.AcceptPatchMiddleware(r, muxhandlers.AcceptPatchConfig{})

r.Use(mw)

Patch Routing Middleware

PatchRoutingMiddleware validates the Content-Type of PATCH requests against a set of allowed patch formats and stores the resolved type in the request context. Non-PATCH requests pass through unchanged. Returns 415 Unsupported Media Type when the Content-Type is missing or unsupported.

Patch Content Type Constants
Constant Value Spec
PatchTypeJSON application/json Implicit merge
PatchTypeMergePatch application/merge-patch+json RFC 7396
PatchTypeJSONPatch application/json-patch+json RFC 6902
PatchRoutingConfig
Field Type Description
AllowedTypes []string Accepted Content-Type values; nil = all three defaults
PatchRouting Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users/{id}", updateUser).Methods(http.MethodPatch)

r.Use(muxhandlers.PatchRoutingMiddleware(muxhandlers.PatchRoutingConfig{}))
Reading the Patch Type from context
func updateUser(w http.ResponseWriter, r *http.Request) {
    switch muxhandlers.PatchContentType(r) {
    case muxhandlers.PatchTypeJSON:
        // implicit merge: sent fields overwrite, absent fields unchanged
    case muxhandlers.PatchTypeMergePatch:
        // RFC 7396: null values mean "remove this field"
    case muxhandlers.PatchTypeJSONPatch:
        // RFC 6902: array of add/remove/replace/move/copy/test operations
    }
}

Redirect Middleware

RedirectMiddleware redirects requests based on path matching rules. It supports exact path matching and prefix matching with a trailing wildcard (*). The first matching rule wins. Non-matching requests pass through. The redirect response includes a Location header and an HTML body with a <meta http-equiv="refresh"> tag for clients that do not follow the Location header automatically.

RedirectRule
Field Type Description
From string Path to match; must start with /; trailing * enables prefix matching
To string Redirect target; suffix appended for wildcard rules; can be an absolute URL
StatusCode int Per-rule HTTP redirect status code; 0 = use config default
RedirectConfig
Field Type Description
Rules []RedirectRule Ordered list of rules; first match wins; required
StatusCode int Default HTTP redirect status code; 0 = 307 Temporary Redirect
Redirect Usage
r := mux.NewRouter()

r.HandleFunc("/swagger/", swaggerHandler).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.RedirectMiddleware(muxhandlers.RedirectConfig{
    Rules: []muxhandlers.RedirectRule{
        {From: "/", To: "/swagger/"},
        {From: "/old-page", To: "/new-page"},
        {From: "/blog/2023/*", To: "/archive/2023/"},
        {From: "/github", To: "https://github.com/example", StatusCode: http.StatusMovedPermanently},
    },
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Canonical Host Middleware

CanonicalHostMiddleware redirects requests to a canonical host URL when the incoming scheme or host does not match. The request path and query string are preserved. Useful for enforcing a single canonical URL (e.g. example.com to www.example.com, or HTTP to HTTPS).

CanonicalHostConfig
Field Type Description
URL string Canonical base URL including scheme and host; required
StatusCode int HTTP redirect status code; 0 = 301 Moved Permanently
CanonicalHost Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.CanonicalHostMiddleware(muxhandlers.CanonicalHostConfig{
    URL: "https://www.example.com",
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

IP Allow Middleware

IPAllowMiddleware restricts access to requests originating from a configured set of IP addresses and CIDR ranges. Requests from IPs not in the allowed list are rejected with 403 Forbidden by default. The client IP is extracted from r.RemoteAddr.

IPAllowConfig
Field Type Description
Allowed []string IP addresses and CIDR ranges that are permitted; required
DeniedHandler http.Handler Custom handler for denied requests; nil = 403 Forbidden with empty body
IPAllow Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

mw, err := muxhandlers.IPAllowMiddleware(muxhandlers.IPAllowConfig{
    Allowed: []string{"10.0.0.0/8", "192.168.0.0/16", "::1"},
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)
IPAllow Usage with custom denied handler
mw, err := muxhandlers.IPAllowMiddleware(muxhandlers.IPAllowConfig{
    Allowed: []string{"10.0.0.0/8"},
    DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusForbidden)
        w.Write([]byte(`{"error":"access denied"}`))
    }),
})
if err != nil {
    log.Fatal(err)
}

r.Use(mw)

Access Log Middleware

AccessLogMiddleware records a structured entry for every request, capturing the response status code and byte count via a wrapped http.ResponseWriter. By default entries are emitted through log/slog (slog.Default when no Logger is provided). The Logger field accepts a fully pre-configured parent *slog.Logger — the middleware inherits its handler, output, format, level, and pre-bound attributes (from Logger.With or Logger.WithGroup), and appends per-request fields to every emitted record. Set LogFunc to bypass slog entirely and route entries to a custom sink.

5xx responses are logged at slog.LevelError; otherwise-Info requests are escalated to slog.LevelWarn when their duration exceeds SlowThreshold. Use Skip to suppress logging for health checks or metrics endpoints. Header capture is opt-in via IncludeHeaders, and Authorization, Cookie, Proxy-Authorization, and Set-Cookie are always redacted when captured.

AccessLogConfig
Field Type Description
Logger *slog.Logger Pre-configured parent; inherits handler/output/format/level and .With/.WithGroup attrs; defaults to slog.Default()
LogFunc func(*AccessLogEntry) Custom sink; bypasses slog entirely when set
Skip func(*mux.Router, *http.Request) bool Return true to suppress logging; receives the router so it can inspect matched route metadata
IncludeHeaders []string Request headers to capture; case-insensitive; nil = none
RedactHeaders []string Additional headers to redact; baseline is Authorization, Cookie, Proxy-Authorization, Set-Cookie
SlowThreshold time.Duration Escalate Info → Warn when handler runs longer than this; 0 = disabled
Now func() time.Time Clock source; nil = time.Now; intended for tests
AccessLogEntry
Field Type Description
Time time.Time When the request started
Method string HTTP method
Proto string r.Proto (HTTP/1.1, HTTP/2.0)
Scheme string Resolved via mux.Scheme(r) (http/https)
Host string r.Host (post-proxy resolution when ProxyHeadersMiddleware is upstream)
Path string r.URL.Path at handler entry
Query string r.URL.RawQuery
Status int Status code; defaults to 200 when handler exits without writing; 0 when Hijacked is true
Hijacked bool True when the handler took over the connection via http.Hijacker (e.g. WebSocket upgrade); slog output emits hijacked=true instead of status
Bytes int64 Response body bytes written
Duration time.Duration Handler execution time
RemoteAddr string r.RemoteAddr (use ProxyHeadersMiddleware to resolve)
UserAgent string User-Agent header
Referer string Referer header
RouteName string Name from mux.Route.Name, if any
RequestID string Result of RequestIDFromContext, if any
Headers map[string]string Captured headers with redaction applied
AccessLog Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

r.Use(muxhandlers.AccessLogMiddleware(r, muxhandlers.AccessLogConfig{
    SlowThreshold: 500 * time.Millisecond,
    Skip: func(_ *mux.Router, req *http.Request) bool {
        return req.URL.Path == "/healthz"
    },
}))
AccessLog Usage with route metadata
const skipLogKey = "access_log_skip"

r := mux.NewRouter()

r.Use(muxhandlers.AccessLogMiddleware(r, muxhandlers.AccessLogConfig{
    Skip: func(_ *mux.Router, req *http.Request) bool {
        route := mux.CurrentRoute(req)
        if route == nil {
            return false
        }
        skip, _ := route.GetMetadataValueOr(skipLogKey, false).(bool)
        return skip
    },
}))

// Tag noisy routes inline; the predicate above suppresses their logs.
r.HandleFunc("/metrics", metricsHandler).Metadata(skipLogKey, true)
r.HandleFunc("/healthz", healthHandler).Metadata(skipLogKey, true)
r.HandleFunc("/api/v1/users", listUsers)
AccessLog Usage with a pre-configured parent logger
// Build the application's parent logger once: handler, output, format,
// level, and any baseline attrs that should appear on every record.
parent := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
})).With(
    slog.String("service", "auth-api"),
    slog.String("env", "prod"),
)

// AccessLogMiddleware inherits the parent's handler and pre-bound attrs;
// every emitted record carries `service` and `env` alongside the
// per-request access-log fields.
r.Use(muxhandlers.AccessLogMiddleware(r, muxhandlers.AccessLogConfig{
    Logger:         parent,
    IncludeHeaders: []string{"X-Tenant-ID", "X-Forwarded-For"},
}))
AccessLog Usage with custom sink
r.Use(muxhandlers.AccessLogMiddleware(r, muxhandlers.AccessLogConfig{
    LogFunc: func(e *muxhandlers.AccessLogEntry) {
        // ship to logging backend
        metrics.RecordRequest(e.RouteName, e.Status, e.Duration)
    },
}))

Graceful Shutdown Middleware

GracefulShutdownMiddleware returns the middleware together with a *Drainer control surface. Requests arriving before Drain() flow through unchanged and are counted in Drainer.InFlight; requests arriving after Drain() receive a 503 Service Unavailable with Connection: close so keep-alive clients reconnect to a healthy peer. Bypass forwards selected requests (typically /healthz, /readyz, /metrics) so the orchestrator can observe the drain. Drainer.Wait blocks until in-flight requests have completed or the supplied context fires, which is the natural pair for http.Server.Shutdown.

GracefulShutdownConfig
Field Type Description
Bypass func(*mux.Router, *http.Request) bool Return true to forward a request to the next handler during drain
Response http.Handler Custom drain response; default headers are still applied before invocation
StatusCode int Status for the default response; defaults to 503
RetryAfter time.Duration Retry-After header as delta-seconds; sub-second values round up to 1
Drainer
Method Description
Drain() Begin rejecting new requests. Idempotent.
IsDraining() bool Report whether Drain has been called
InFlight() int64 Number of requests currently inside the middleware
Wait(ctx context.Context) error Block until InFlight reaches zero or ctx is cancelled; returns ctx.Err() on cancel
Graceful Shutdown Usage
r := mux.NewRouter()

mw, drainer := muxhandlers.GracefulShutdownMiddleware(r, muxhandlers.GracefulShutdownConfig{
    RetryAfter: 15 * time.Second,
    Bypass: func(_ *mux.Router, req *http.Request) bool {
        return req.URL.Path == "/healthz" || req.URL.Path == "/readyz"
    },
})
r.Use(mw)

srv := &http.Server{Addr: ":8080", Handler: r}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}()

stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop

drainer.Drain()

shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := drainer.Wait(shutdownCtx); err != nil {
    log.Printf("drain timed out with %d requests still in flight", drainer.InFlight())
}
_ = srv.Shutdown(shutdownCtx)
Graceful Shutdown Usage with custom response
mw, drainer := muxhandlers.GracefulShutdownMiddleware(r, muxhandlers.GracefulShutdownConfig{
    Response: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusServiceUnavailable)
        _, _ = w.Write([]byte(`{"draining":true}`))
    }),
})

r.Use(mw)

Maintenance Mode Middleware

MaintenanceModeMiddleware short-circuits matching requests with a 503 Service Unavailable response (RFC 9110 Section 15.6.4) while a maintenance window is active. The Enabled predicate is the single source of truth; back it with whatever you like (atomic.Bool, file presence, env var, cron window). Bypass lets specific requests through during maintenance (admin IPs, deploy tooling, health checks). Response, when set, fully owns the response body so callers can render an HTML maintenance page, return RFC 9457 problem JSON, or redirect to a static page. RetryAfter / RetryAt populate the Retry-After header in either delta-seconds or HTTP-date form (RFC 9110 Section 10.2.3).

MaintenanceConfig
Field Type Description
Enabled func(*http.Request) bool Required; returning true triggers maintenance for the request. nil = middleware is a no-op
Bypass func(*mux.Router, *http.Request) bool Return true to forward the request to the next handler even while Enabled is true
Response http.Handler Custom maintenance response; when set, StatusCode is ignored
StatusCode int Status for the default response; defaults to 503
RetryAfter time.Duration Retry-After as delta-seconds; sub-second values round up to 1
RetryAt time.Time Retry-After as HTTP-date in UTC; takes precedence over RetryAfter
Maintenance Usage with atomic.Bool
var inMaintenance atomic.Bool

r := mux.NewRouter()

r.Use(muxhandlers.MaintenanceModeMiddleware(r, muxhandlers.MaintenanceConfig{
    Enabled:    func(_ *http.Request) bool { return inMaintenance.Load() },
    RetryAfter: 5 * time.Minute,
}))

// Flip from anywhere: signal handler, admin endpoint, deploy script, etc.
inMaintenance.Store(true)
Maintenance Usage with custom HTML page
tmpl := template.Must(template.New("maint").Parse(`<!doctype html>
<title>Maintenance</title>
<h1>We'll be right back</h1>
<p>Estimated end: {{.End}}</p>`))

end := time.Now().Add(15 * time.Minute)

r.Use(muxhandlers.MaintenanceModeMiddleware(r, muxhandlers.MaintenanceConfig{
    Enabled: func(_ *http.Request) bool { return inMaintenance.Load() },
    RetryAt: end,
    Response: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        w.WriteHeader(http.StatusServiceUnavailable)
        _ = tmpl.Execute(w, struct{ End string }{end.UTC().Format(time.RFC1123)})
    }),
}))
Maintenance Usage with admin bypass and route metadata
const exemptKey = "exempt_from_maintenance"

r.Use(muxhandlers.MaintenanceModeMiddleware(r, muxhandlers.MaintenanceConfig{
    Enabled: func(_ *http.Request) bool { return inMaintenance.Load() },
    Bypass: func(_ *mux.Router, req *http.Request) bool {
        if req.Header.Get("X-Admin-Token") == adminToken {
            return true
        }
        route := mux.CurrentRoute(req)
        if route == nil {
            return false
        }
        exempt, _ := route.GetMetadataValueOr(exemptKey, false).(bool)
        return exempt
    },
}))

// Health checks and metrics scrapes stay reachable during maintenance.
r.HandleFunc("/healthz", healthHandler).Metadata(exemptKey, true)
r.HandleFunc("/metrics", metricsHandler).Metadata(exemptKey, true)
r.HandleFunc("/api/v1/users", listUsers)

No-Cache Middleware

NoCacheMiddleware forces responses to be uncacheable. It rewrites caching headers on the response writer at the moment the handler flushes its status line, overriding any Cache-Control, Pragma, or Expires the handler may have set, and removes ETag and Last-Modified so downstream caches cannot perform conditional revalidation. The Modern preset emits Cache-Control: no-store per RFC 9111 Section 5.2.2.5; Strict adds the legacy Pragma and Expires combo for HTTP/1.0-era intermediaries.

NoCachePreset
Preset Emitted Headers
NoCachePresetModern (default) Cache-Control: no-store
NoCachePresetStrict Cache-Control: no-store, no-cache, must-revalidate, max-age=0, private; Pragma: no-cache; Expires: 0

Both presets strip ETag and Last-Modified from the response.

NoCacheConfig
Field Type Description
Preset NoCachePreset Header set to emit; defaults to NoCachePresetModern
Skip func(*mux.Router, *http.Request) bool Return true to leave the handler's caching headers untouched
NoCache Usage
r := mux.NewRouter()

r.HandleFunc("/api/v1/users", listUsers).Methods(http.MethodGet)

r.Use(muxhandlers.NoCacheMiddleware(r, muxhandlers.NoCacheConfig{}))
NoCache Usage with strict preset
r.Use(muxhandlers.NoCacheMiddleware(r, muxhandlers.NoCacheConfig{
    Preset: muxhandlers.NoCachePresetStrict,
}))
NoCache Usage with route metadata opt-out
const allowCacheKey = "allow_cache"

r := mux.NewRouter()

r.Use(muxhandlers.NoCacheMiddleware(r, muxhandlers.NoCacheConfig{
    Skip: func(_ *mux.Router, req *http.Request) bool {
        route := mux.CurrentRoute(req)
        if route == nil {
            return false
        }
        allow, _ := route.GetMetadataValueOr(allowCacheKey, false).(bool)
        return allow
    },
}))

// Most routes get no-store; tag specific ones to keep their own cache headers.
r.HandleFunc("/api/v1/users", dynamicHandler)
r.HandleFunc("/assets/logo.png", staticHandler).Metadata(allowCacheKey, true)

HTCPCP Middleware

HTCPCPMiddleware implements the Hyper Text Coffee Pot Control Protocol (RFC 2324) extended for tea efflux appliances (RFC 7168). It intercepts BREW and WHEN requests and responds according to the configured pot type. Non-HTCPCP methods pass through unchanged.

By default the middleware only activates on April 1 (the publication date of both RFCs); on every other day it becomes a no-op. Override ActiveOn to force-enable the protocol or restrict it further.

Response Matrix
Pot Request Response
Teapot BREW without tea-* addition 418 I'm a Teapot
Teapot BREW with supported tea-* variety 200 + message/teapot
Teapot BREW with unsupported tea variety 406 Not Acceptable
Coffee pot BREW with tea-* addition 406 Not Acceptable
Coffee pot BREW without tea 200 + message/coffeepot
Either BREW with Empty: true 503 + Retry-After
Either BREW with addition not in AvailableAdditions 406 Not Acceptable
Either WHEN 200 + message/coffeepot
Either Other methods (GET, POST, ...) passthrough
HTCPCPConfig
Field Type Description
PotType PotType PotCoffee (default) or PotTeapot
Teas []string Tea varieties this teapot can brew; nil for a teapot uses DefaultTeaVarieties
AvailableAdditions []string Available additions per RFC 2324 Section 2.2.2.1; requests for other additions return 406
Empty bool When true, BREW returns 503 with Retry-After
RetryAfter int Retry-After value in seconds; defaults to 60
ActiveOn func(time.Time) bool Predicate gating the middleware; nil = IsAprilFirst
Now func() time.Time Clock source; nil = time.Now; intended for tests
DefaultTeaVarieties

black, chai, earl-grey, english-breakfast, green, jasmine, oolong, peppermint, rooibos (RFC 7168 Section 2.1.1).

HTCPCP Usage
r := mux.NewRouter()

r.Route("/pot", func(pot *mux.Router) {
    pot.Use(muxhandlers.HTCPCPMiddleware(muxhandlers.HTCPCPConfig{
        PotType: muxhandlers.PotTeapot,
        Teas:    []string{"earl-grey", "rooibos"},
    }))
    pot.HandleFunc("/", potStatusHandler)
})
HTCPCP Usage with always-on override
mw := muxhandlers.HTCPCPMiddleware(muxhandlers.HTCPCPConfig{
    PotType:  muxhandlers.PotCoffee,
    ActiveOn: func(_ time.Time) bool { return true }, // year-round
})

r.Use(mw)

Documentation

Overview

Package muxhandlers provides HTTP middleware handlers for the mux router.

CORS Middleware

CORSMiddleware implements the full CORS protocol per the Fetch Standard. It validates the Origin header (RFC 6454), handles preflight OPTIONS requests, and sets the appropriate response headers.

mw, err := muxhandlers.CORSMiddleware(r, muxhandlers.CORSConfig{
    AllowedOrigins:   []string{"https://example.com"},
    AllowCredentials: true,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Basic Auth Middleware

BasicAuthMiddleware implements HTTP Basic Authentication per RFC 7617. Credentials can be validated via a dynamic callback or a static map. Static credential comparison uses constant-time comparison to prevent timing attacks.

mw, err := muxhandlers.BasicAuthMiddleware(muxhandlers.BasicAuthConfig{
    Realm: "My App",
    Credentials: map[string]string{
        "admin": "secret",
    },
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Bearer Auth Middleware

BearerAuthMiddleware implements HTTP Bearer Token Authentication per RFC 6750. It extracts the token from the Authorization header and validates it using a user-provided function. When the token is missing, malformed, or invalid, the middleware responds with 401 Unauthorized and a WWW-Authenticate: Bearer header per RFC 6750 Section 3. The ValidateFunc receives the full request, allowing token validation based on route variables, headers, or other request context.

mw, err := muxhandlers.BearerAuthMiddleware(muxhandlers.BearerAuthConfig{
    Realm: "My API",
    ValidateFunc: func(r *http.Request, token string) bool {
        return token == expectedToken
    },
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Proxy Headers Middleware

ProxyHeadersMiddleware populates request fields from reverse proxy headers when the request originates from a trusted proxy. It sets r.RemoteAddr from X-Forwarded-For or X-Real-IP, r.URL.Scheme from X-Forwarded-Proto or X-Forwarded-Scheme, and r.Host from X-Forwarded-Host. When EnableForwarded is true, the RFC 7239 Forwarded header is also parsed as a lowest-priority fallback. A trusted proxy list (IPs and CIDRs) restricts which peers are allowed to set these headers, preventing spoofing from untrusted clients. When TrustedProxies is empty, DefaultTrustedProxies (RFC 1918, RFC 4193, and loopback ranges) is used.

mw, err := muxhandlers.ProxyHeadersMiddleware(muxhandlers.ProxyHeadersConfig{
    TrustedProxies:  []string{"10.0.0.0/8", "172.16.0.0/12"},
    EnableForwarded: true,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Recovery Middleware

RecoveryMiddleware recovers from panics in downstream handlers, returns 500 Internal Server Error to the client, and optionally invokes a custom log function with the request and recovered value.

r.Use(muxhandlers.RecoveryMiddleware(muxhandlers.RecoveryConfig{
    LogFunc: func(r *http.Request, err any) {
        log.Printf("panic: %v %s", err, r.URL.Path)
    },
}))

Request ID Middleware

RequestIDMiddleware generates or propagates a unique request identifier. The ID is set on the request header, the response header, and the request context. Downstream handlers can retrieve it with RequestIDFromContext. By default it generates UUID v4 values using github.com/google/uuid. Use GenerateUUIDv7 for time-ordered IDs (RFC 9562). The GenerateFunc receives the current request, allowing ID generation based on request context.

r.Use(muxhandlers.RequestIDMiddleware(muxhandlers.RequestIDConfig{
    TrustIncoming: true,
}))

Time-ordered UUID v7:

r.Use(muxhandlers.RequestIDMiddleware(muxhandlers.RequestIDConfig{
    GenerateFunc: muxhandlers.GenerateUUIDv7,
}))

Request Size Limit Middleware

RequestSizeLimitMiddleware rejects request bodies that exceed a maximum size. It wraps r.Body with http.MaxBytesReader, which returns 413 Request Entity Too Large when the limit is exceeded.

mw, err := muxhandlers.RequestSizeLimitMiddleware(muxhandlers.RequestSizeLimitConfig{
    MaxBytes: 1 << 20, // 1 MiB
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Timeout Middleware

TimeoutMiddleware limits handler execution time by wrapping the handler with http.TimeoutHandler. It returns 503 Service Unavailable when the handler does not complete within the configured duration.

mw, err := muxhandlers.TimeoutMiddleware(muxhandlers.TimeoutConfig{
    Duration: 30 * time.Second,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Compression Middleware

CompressionMiddleware compresses response bodies using gzip or deflate when the client advertises support via the Accept-Encoding header. Gzip is preferred over deflate when both are accepted. It uses sync.Pool instances to reuse writers for performance. Compression is skipped for inherently compressed content types (images, video, audio, archives).

mw, err := muxhandlers.CompressionMiddleware(muxhandlers.CompressionConfig{
    Level:     gzip.BestSpeed,
    MinLength: 1024,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Security Headers Middleware

SecurityHeadersMiddleware sets common security response headers with sensible defaults. Headers are set before calling the next handler. By default it sets X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Referrer-Policy: strict-origin-when-cross-origin. HSTS, CSP, Permissions-Policy, and Cross-Origin-Opener-Policy headers are opt-in.

mw, err := muxhandlers.SecurityHeadersMiddleware(muxhandlers.SecurityHeadersConfig{
    HSTSMaxAge:            63072000,
    HSTSIncludeSubDomains: true,
    HSTSPreload:           true,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Method Override Middleware

MethodOverrideMiddleware allows clients to override the HTTP method via a configurable header. By default only POST requests are eligible for override; use OriginalMethods to allow other methods. The first non-empty header value from HeaderNames is uppercased and checked against the allowed set. When allowed, r.Method is updated and the header is removed from the request. By default it checks X-HTTP-Method-Override, X-Method-Override, and X-HTTP-Method in that order.

mw, err := muxhandlers.MethodOverrideMiddleware(muxhandlers.MethodOverrideConfig{})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Content-Type Check Middleware

ContentTypeCheckMiddleware validates that requests carry a matching Content-Type header. Matching is case-insensitive and ignores parameters such as charset. It returns 415 Unsupported Media Type when the Content-Type is missing or does not match any of the allowed types. By default it checks POST, PUT, and PATCH requests.

mw, err := muxhandlers.ContentTypeCheckMiddleware(muxhandlers.ContentTypeCheckConfig{
    AllowedTypes: []string{"application/json"},
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Server Middleware

ServerMiddleware sets server identification response headers. It sets X-Server-Hostname with the machine hostname, resolved once at factory time via os.Hostname. Use the Hostname field to provide a static value instead.

mw, err := muxhandlers.ServerMiddleware(muxhandlers.ServerConfig{})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Cache-Control Middleware

CacheControlMiddleware sets Cache-Control and Expires response headers based on the response Content-Type. Rules are evaluated in order; the first rule whose ContentType prefix matches wins. If no rule matches and DefaultValue/DefaultExpires is non-empty, it is used. When the handler already sets a Cache-Control or Expires header, the middleware does not overwrite the respective header.

mw, err := muxhandlers.CacheControlMiddleware(muxhandlers.CacheControlConfig{
    Rules: []muxhandlers.CacheControlRule{
        {ContentType: "image/", Value: "public, max-age=86400", Expires: 24 * time.Hour},
        {ContentType: "application/json", Value: "no-cache", Expires: 0},
    },
    DefaultValue:   "no-store",
    DefaultExpires: 0,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Static Files Handler

StaticFilesHandler serves static files from any fs.FS implementation (os.DirFS, embed.FS, fstest.MapFS, etc.) using http.FileServerFS. It is not middleware — it returns an http.Handler that serves files directly. Directory listing is disabled by default; when a directory has no index.html, a 404 is returned instead of a file listing. When SPAFallback is enabled, requests for non-existent paths serve the root index.html, allowing client-side routers to handle routing. PathPrefix strips the URL prefix internally, replacing http.StripPrefix. Aliases map custom URL paths to specific files in the FS.

handler, err := muxhandlers.StaticFilesHandler(muxhandlers.StaticFilesConfig{
    FS:         staticFS,
    PathPrefix: "/ui",
    EnableETag: true,
    Aliases: map[string]string{
        "/policy-builder/":    "policy-builder.html",
        "/policy-playground/": "policy-playground.html",
    },
})
if err != nil {
    log.Fatal(err)
}
r.PathPrefix("/ui/").Handler(handler)

Profiler Handler

RegisterProfiler registers the standard net/http/pprof and expvar endpoints on the given router. It is not middleware — it registers routes directly. Endpoints use the standard /debug/pprof/ and /debug/vars paths. Mount with any prefix using Route:

r.Route("/_internal", muxhandlers.RegisterProfiler)
// serves /_internal/debug/pprof/, /_internal/debug/vars, etc.

Sunset Middleware

SunsetMiddleware sets the Sunset response header per RFC 8594 to indicate that a resource will become unresponsive at a specific point in time. Optionally sets the Deprecation header and a Link header with rel="sunset".

mw, err := muxhandlers.SunsetMiddleware(muxhandlers.SunsetConfig{
    Sunset:      time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
    Deprecation: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
    Link:        "https://example.com/docs/migration",
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Idempotency Middleware

IdempotencyMiddleware caches responses keyed by the Idempotency-Key header per draft-ietf-httpapi-idempotency-key-header. Duplicate requests with the same key replay the cached response without invoking the handler. The middleware requires an IdempotencyStore implementation for persistence (e.g. Redis, in-memory).

mw, err := muxhandlers.IdempotencyMiddleware(muxhandlers.IdempotencyConfig{
    Store: redisStore,
    TTL:   1 * time.Hour,
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Content Negotiation Middleware

ContentNegotiationMiddleware performs proactive content negotiation per RFC 9110 Section 12.5.1. It parses the Accept header with quality values, selects the best matching type from the offered list, and stores the result in the request context. Use NegotiatedType to retrieve the selected type inside a handler. When Offered is empty, any media type is accepted. When no offered type matches, the middleware responds with 406 Not Acceptable.

r.Use(muxhandlers.ContentNegotiationMiddleware(muxhandlers.ContentNegotiationConfig{
    Offered: []string{"application/json", "application/xml", "text/html"},
}))

Inside a handler:

func handler(w http.ResponseWriter, r *http.Request) {
    switch muxhandlers.NegotiatedType(r) {
    case "application/json":
        mux.ResponseJSON(w, http.StatusOK, data)
    case "application/xml":
        mux.ResponseXML(w, http.StatusOK, data)
    }
}

Problem Details

WriteProblemDetails writes an RFC 9457 Problem Details JSON response with Content-Type "application/problem+json". The ProblemDetails struct contains the standard members (type, title, status, detail, instance) and supports extension members via the Extensions map. NewProblemDetails creates a ProblemDetails with the status code and standard status text as title.

muxhandlers.WriteProblemDetails(w, muxhandlers.ProblemDetails{
    Type:   "https://example.com/errors/not-found",
    Title:  "Resource not found",
    Status: http.StatusNotFound,
    Detail: "User with ID 42 was not found",
})

With extensions:

muxhandlers.WriteProblemDetails(w, muxhandlers.ProblemDetails{
    Type:   "https://example.com/errors/validation",
    Title:  "Validation Error",
    Status: http.StatusUnprocessableEntity,
    Extensions: map[string]any{
        "errors": []string{"email is required"},
    },
})

Quick error response:

muxhandlers.WriteProblemDetails(w, muxhandlers.NewProblemDetails(http.StatusForbidden))

Early Hints Middleware

EarlyHintsMiddleware sends a 103 Early Hints informational response per RFC 8297 before the final response. This allows clients to begin preloading resources (stylesheets, scripts, fonts) while the server is still processing the request. The configured Link headers are sent with the 103 response and are not carried over to the final response.

mw, err := muxhandlers.EarlyHintsMiddleware(muxhandlers.EarlyHintsConfig{
    Links: []string{
        `</style.css>; rel=preload; as=style`,
        `</app.js>; rel=preload; as=script`,
    },
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Redirect Middleware

RedirectMiddleware redirects requests based on path matching rules. It supports exact path matching and prefix matching with a trailing wildcard ("*"). The first matching rule wins. Non-matching requests pass through. The redirect response includes a Location header and an HTML body with a <meta http-equiv="refresh"> tag for clients that do not follow the Location header automatically.

mw, err := muxhandlers.RedirectMiddleware(muxhandlers.RedirectConfig{
    Rules: []muxhandlers.RedirectRule{
        {From: "/", To: "/swagger/"},
        {From: "/old-page", To: "/new-page"},
        {From: "/blog/2023/*", To: "/archive/2023/"},
        {From: "/github", To: "https://github.com/example", StatusCode: http.StatusTemporaryRedirect},
    },
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

IP Allow Middleware

IPAllowMiddleware restricts access to requests originating from a configured set of IP addresses and CIDR ranges. Requests from IPs not in the allowed list are rejected with 403 Forbidden by default. Use DeniedHandler to customize the error response.

mw, err := muxhandlers.IPAllowMiddleware(muxhandlers.IPAllowConfig{
    Allowed: []string{"10.0.0.0/8", "192.168.0.0/16"},
})
if err != nil {
    log.Fatal(err)
}
r.Use(mw)

Access Log Middleware

AccessLogMiddleware records a structured entry for every request, capturing the response status code and byte count via a wrapped http.ResponseWriter. By default entries are emitted through log/slog (slog.Default when no Logger is provided). The Logger field accepts a fully pre-configured parent *slog.Logger: the middleware inherits its handler, output, format, level, and pre-bound attributes (from Logger.With or Logger.WithGroup), and appends per-request fields to every emitted record. Set LogFunc to bypass slog entirely and route entries to a custom sink.

5xx responses are logged at Error level; otherwise-Info requests are escalated to Warn when their duration exceeds SlowThreshold. Use Skip to suppress logging for health checks or metrics endpoints. Header capture is opt-in via IncludeHeaders, and Authorization, Cookie, Proxy-Authorization, and Set-Cookie are always redacted when captured.

r := mux.NewRouter()
r.Use(muxhandlers.AccessLogMiddleware(r, muxhandlers.AccessLogConfig{
    SlowThreshold: 500 * time.Millisecond,
    Skip: func(router *mux.Router, req *http.Request) bool {
        return req.URL.Path == "/healthz"
    },
}))

Graceful Shutdown Middleware

GracefulShutdownMiddleware intercepts new requests once Drain has been called and a Drainer is the control surface returned alongside the middleware. Requests arriving before Drain() flow through unchanged and are counted in Drainer.InFlight; requests arriving after Drain() receive a 503 with Connection: close (RFC 9110 Sections 15.6.4 and 7.6.1 respectively) so keep-alive clients reconnect to a healthy peer. Bypass forwards selected requests (typically /healthz, /readyz, /metrics) so the orchestrator can observe the drain. Drainer.Wait blocks until in-flight requests have completed or the supplied context fires.

mw, drainer := muxhandlers.GracefulShutdownMiddleware(r, muxhandlers.GracefulShutdownConfig{
    RetryAfter: 15 * time.Second,
    Bypass: func(_ *mux.Router, req *http.Request) bool {
        return req.URL.Path == "/healthz" || req.URL.Path == "/readyz"
    },
})
r.Use(mw)

stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop

drainer.Drain()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = drainer.Wait(ctx)
_ = srv.Shutdown(ctx)

Maintenance Mode Middleware

MaintenanceModeMiddleware short-circuits matching requests with a 503 Service Unavailable response (RFC 9110 Section 15.6.4) while a maintenance window is active. The Enabled predicate is the single source of truth; callers back it with whatever they like (atomic.Bool, file presence, env var, cron window) and the middleware reads it per request. Bypass lets specific requests through during maintenance (admin IPs, deploy tooling, health checks). Response, when set, fully owns the response body so the caller can render an HTML maintenance page, return RFC 9457 ProblemDetails JSON, or redirect to a static page. RetryAfter / RetryAt populate the Retry-After header in either delta-seconds or HTTP-date form.

var inMaintenance atomic.Bool

r.Use(muxhandlers.MaintenanceModeMiddleware(r, muxhandlers.MaintenanceConfig{
    Enabled:    func(_ *http.Request) bool { return inMaintenance.Load() },
    RetryAfter: 5 * time.Minute,
    Bypass: func(_ *mux.Router, req *http.Request) bool {
        return req.Header.Get("X-Admin-Token") == adminToken
    },
}))

No-Cache Middleware

NoCacheMiddleware forces responses to be uncacheable. It rewrites caching headers on the response writer at the moment the handler flushes its status line, overriding any Cache-Control, Pragma, or Expires the handler may have set, and removes ETag and Last-Modified so downstream caches cannot perform conditional revalidation. The Modern preset emits Cache-Control: no-store per RFC 9111 Section 5.2.2.5; Strict adds the legacy Pragma and Expires combo for HTTP/1.0-era intermediaries.

r := mux.NewRouter()
r.Use(muxhandlers.NoCacheMiddleware(r, muxhandlers.NoCacheConfig{
    Preset: muxhandlers.NoCachePresetStrict,
    Skip: func(_ *mux.Router, req *http.Request) bool {
        return strings.HasPrefix(req.URL.Path, "/assets/")
    },
}))

HTCPCP Middleware

HTCPCPMiddleware implements the Hyper Text Coffee Pot Control Protocol (RFC 2324) extended for tea efflux appliances (RFC 7168). It intercepts BREW and WHEN requests and responds according to the configured pot type. A teapot asked to brew coffee returns 418 I'm a Teapot; a coffee pot asked for tea returns 406 Not Acceptable; an empty pot returns 503 Service Unavailable with Retry-After. Non-HTCPCP methods pass through unchanged.

By default the middleware only activates on April 1 (the publication date of both RFCs); on every other day it becomes a no-op. Override ActiveOn to force-enable the protocol or restrict it further.

r.Route("/pot", func(pot *mux.Router) {
    pot.Use(muxhandlers.HTCPCPMiddleware(muxhandlers.HTCPCPConfig{
        PotType: muxhandlers.PotTeapot,
        Teas:    []string{"earl-grey", "rooibos"},
    }))
    pot.HandleFunc("/", potStatusHandler)
})

Index

Constants

View Source
const (
	MethodBrew = "BREW"
	MethodWhen = "WHEN"
)

HTCPCP method tokens defined by RFC 2324 Section 2.1.1 and extended for tea by RFC 7168 Section 2.1.1.

View Source
const (
	// PatchTypeJSON is the implicit merge patch using standard JSON.
	PatchTypeJSON = "application/json"

	// PatchTypeMergePatch is the JSON Merge Patch format per RFC 7396.
	PatchTypeMergePatch = "application/merge-patch+json"

	// PatchTypeJSONPatch is the JSON Patch format per RFC 6902.
	PatchTypeJSONPatch = "application/json-patch+json"
)

Patch content type constants for the supported PATCH formats.

View Source
const ContentTypeMessageCoffeePot = "message/coffeepot"

ContentTypeMessageCoffeePot is the message/coffeepot media type returned by a coffee pot to a successful BREW (RFC 2324 Section 4).

View Source
const ContentTypeMessageTeapot = "message/teapot"

ContentTypeMessageTeapot is the message/teapot media type returned by a teapot to a successful BREW (RFC 7168 Section 2.3.1).

View Source
const IdempotencySkipMetadataKey = "idempotency:skip"

IdempotencySkipMetadataKey is the route metadata key used to skip idempotency processing for specific routes. Set this key to true in route metadata to bypass the middleware.

r.HandleFunc("/health", handler).Metadata(muxhandlers.IdempotencySkipMetadataKey, true)
View Source
const StatusImATeapot = http.StatusTeapot

StatusImATeapot is the HTCPCP status code returned when a teapot is asked to brew coffee (RFC 2324 Section 2.3.2, RFC 7168 Section 2.3.3). Mirrors http.StatusTeapot from the standard library for callers that prefer the protocol-native name.

Variables

View Source
var DefaultTeaVarieties = []string{
	"black",
	"chai",
	"earl-grey",
	"english-breakfast",
	"green",
	"jasmine",
	"oolong",
	"peppermint",
	"rooibos",
}

DefaultTeaVarieties is the tea variety registry from RFC 7168 Section 2.1.1, used when HTCPCPConfig.Teas is nil for a teapot.

View Source
var DefaultTrustedProxies = []string{
	"127.0.0.0/8",
	"10.0.0.0/8",
	"172.16.0.0/12",
	"192.168.0.0/16",
	"100.64.0.0/10",
	"::1/128",
	"fc00::/7",
}

DefaultTrustedProxies is the set of private and loopback ranges used when ProxyHeadersConfig.TrustedProxies is empty.

Included ranges:

  • 127.0.0.0/8 — IPv4 loopback (RFC 1122)
  • 10.0.0.0/8 — Class A private (RFC 1918)
  • 172.16.0.0/12 — Class B private (RFC 1918)
  • 192.168.0.0/16 — Class C private (RFC 1918)
  • 100.64.0.0/10 — CGNAT shared address space (RFC 6598)
  • ::1/128 — IPv6 loopback (RFC 4291)
  • fc00::/7 — IPv6 unique local (RFC 4193)
View Source
var ErrCanonicalHostEmpty = errors.New("canonical host: URL must not be empty")

ErrCanonicalHostEmpty is returned when CanonicalHostConfig.URL is empty.

View Source
var ErrCanonicalHostInvalid = errors.New("canonical host: URL is not valid")

ErrCanonicalHostInvalid is returned when CanonicalHostConfig.URL cannot be parsed.

View Source
var ErrIPAllowEmpty = errors.New("ip allow: allowed list must not be empty")

ErrIPAllowEmpty is returned when IPAllowConfig.Allowed is empty.

View Source
var ErrIPAllowInvalidEntry = errors.New("ip allow: invalid entry")

ErrIPAllowInvalidEntry is returned when an Allowed entry is neither a valid IP address nor a valid CIDR range.

View Source
var ErrInvalidCompressionLevel = errors.New("compression: invalid compression level")

ErrInvalidCompressionLevel is returned when CompressionConfig.Level is outside the valid compression level range.

View Source
var ErrInvalidFrameOption = errors.New("security headers: frame option must be DENY, SAMEORIGIN, or empty")

ErrInvalidFrameOption is returned when SecurityHeadersConfig.FrameOption is not one of the valid values: "DENY", "SAMEORIGIN", or empty string.

View Source
var ErrInvalidMaxSize = errors.New("request size limit: max size must be greater than zero")

ErrInvalidMaxSize is returned when RequestSizeLimitConfig.MaxBytes is not greater than zero.

View Source
var ErrInvalidOverrideMethod = errors.New("method override: allowed methods must be valid HTTP methods")

ErrInvalidOverrideMethod is returned when MethodOverrideConfig.AllowedMethods or MethodOverrideConfig.OriginalMethods contains an invalid HTTP method.

View Source
var ErrInvalidProxy = errors.New("proxy headers: invalid proxy entry")

ErrInvalidProxy is returned when a TrustedProxies entry is neither a valid IP address nor a valid CIDR range.

View Source
var ErrInvalidTimeout = errors.New("timeout: duration must be greater than zero")

ErrInvalidTimeout is returned when TimeoutConfig.Duration is not greater than zero.

View Source
var ErrNoAllowedTypes = errors.New("content type check: at least one allowed content type is required")

ErrNoAllowedTypes is returned when ContentTypeCheckConfig.AllowedTypes is empty.

View Source
var ErrNoAuthSource = errors.New("basic auth: at least one of ValidateFunc or Credentials must be set")

ErrNoAuthSource is returned when BasicAuthConfig has neither ValidateFunc nor Credentials configured.

View Source
var ErrNoCacheControlRules = errors.New("cache control: at least one rule is required")

ErrNoCacheControlRules is returned when CacheControlConfig.Rules is empty.

View Source
var ErrNoIdempotencyStore = errors.New("idempotency: Store must be set")

ErrNoIdempotencyStore is returned when IdempotencyConfig.Store is nil.

View Source
var ErrNoLinks = errors.New("early hints: at least one Link must be set")

ErrNoLinks is returned when EarlyHintsConfig has neither Links nor LinksFunc configured.

View Source
var ErrNoTokenValidator = errors.New("bearer auth: ValidateFunc must be set")

ErrNoTokenValidator is returned when BearerAuthConfig has no ValidateFunc configured.

View Source
var ErrRedirectEmptyFrom = errors.New("redirect: rule From must not be empty")

ErrRedirectEmptyFrom is returned when a RedirectRule has an empty From field.

View Source
var ErrRedirectEmptyTo = errors.New("redirect: rule To must not be empty")

ErrRedirectEmptyTo is returned when a RedirectRule has an empty To field.

View Source
var ErrRedirectFromNoSlash = errors.New("redirect: rule From must start with /")

ErrRedirectFromNoSlash is returned when a RedirectRule.From does not start with "/".

View Source
var ErrRedirectNoRules = errors.New("redirect: rules must not be empty")

ErrRedirectNoRules is returned when RedirectConfig.Rules is empty.

View Source
var ErrStaticFilesAliasTargetNotFound = errors.New("static files: alias target file not found")

ErrStaticFilesAliasTargetNotFound is returned when an alias target file does not exist in the file system.

View Source
var ErrStaticFilesNoFS = errors.New("static files: file system must not be nil")

ErrStaticFilesNoFS is returned when StaticFilesConfig.FS is nil.

View Source
var ErrStaticFilesNoIndexHTML = errors.New("static files: index.html is required when SPA fallback is enabled")

ErrStaticFilesNoIndexHTML is returned when SPAFallback is enabled but the file system does not contain an index.html at the root.

View Source
var ErrSunsetZeroTime = errors.New("sunset: sunset time must not be zero")

ErrSunsetZeroTime is returned when SunsetConfig.Sunset is the zero time.

View Source
var ErrWildcardCredentials = errors.New("wildcard origin \"*\" cannot be used with AllowCredentials; use AllowOriginFunc instead")

ErrWildcardCredentials is returned when AllowedOrigins contains "*" and AllowCredentials is true. Use AllowOriginFunc for dynamic origin checks with credentials.

Functions

func AcceptPatchMiddleware added in v0.7.0

func AcceptPatchMiddleware(router *mux.Router, cfg AcceptPatchConfig) mux.MiddlewareFunc

AcceptPatchMiddleware returns a middleware that handles OPTIONS requests by responding with Allow and Accept-Patch headers per RFC 5789. The Allow header is auto-discovered from the router's registered methods for the matched path. Non-OPTIONS requests pass through unchanged.

Because the router middleware only runs for matched routes, this function also sets the router's MethodNotAllowedHandler to intercept OPTIONS requests that would otherwise receive a 405 response.

Spec reference: https://www.rfc-editor.org/rfc/rfc5789#section-3.1

func AccessLogMiddleware added in v0.15.0

func AccessLogMiddleware(router *mux.Router, cfg AccessLogConfig) mux.MiddlewareFunc

AccessLogMiddleware records a structured entry for every request. The middleware wraps the response writer to capture the status code and response body byte count, runs the next handler, then emits an AccessLogEntry to LogFunc (when set) or to Logger (a slog logger). When neither is set, slog.Default() receives the entry.

5xx responses are logged at slog.LevelError; SlowThreshold escalates otherwise-Info requests to slog.LevelWarn when the handler runs longer than the threshold. Header capture is opt-in via IncludeHeaders; sensitive headers (Authorization, Cookie, Proxy-Authorization, Set-Cookie, plus anything in RedactHeaders) are always replaced by "[REDACTED]" when captured.

The router is accepted so the Skip predicate can resolve route metadata or use route-aware filtering. Pass the same *mux.Router the middleware is attached to via Use.

func BasicAuthMiddleware

func BasicAuthMiddleware(cfg BasicAuthConfig) (mux.MiddlewareFunc, error)

BasicAuthMiddleware returns a middleware that implements HTTP Basic Authentication per RFC 7617. It validates the Authorization header and responds with 401 Unauthorized when credentials are missing or invalid.

It returns ErrNoAuthSource if both ValidateFunc and Credentials are nil/empty.

func BearerAuthMiddleware added in v0.7.0

func BearerAuthMiddleware(cfg BearerAuthConfig) (mux.MiddlewareFunc, error)

BearerAuthMiddleware returns a middleware that implements HTTP Bearer Token Authentication per RFC 6750. It extracts the token from the Authorization header and validates it using the configured ValidateFunc.

When the Authorization header is missing, malformed, or the token is invalid, the middleware responds with 401 Unauthorized and a WWW-Authenticate: Bearer header per RFC 6750 Section 3.

It returns ErrNoTokenValidator if ValidateFunc is nil.

func CORSMiddleware

func CORSMiddleware(r *mux.Router, cfg CORSConfig) (mux.MiddlewareFunc, error)

CORSMiddleware returns a middleware that implements the full CORS protocol per the Fetch Standard (https://fetch.spec.whatwg.org/#http-cors-protocol). It validates the Origin header (RFC 6454), handles preflight OPTIONS requests, and sets the appropriate response headers.

It returns an error if the configuration is invalid (e.g. wildcard origin combined with AllowCredentials).

Because the router middleware only runs for matched routes, this function also sets the router's MethodNotAllowedHandler to intercept CORS preflight OPTIONS requests that would otherwise receive a 405 response.

func CacheControlMiddleware added in v0.2.0

func CacheControlMiddleware(cfg CacheControlConfig) (mux.MiddlewareFunc, error)

CacheControlMiddleware returns a middleware that sets Cache-Control and Expires response headers based on the response Content-Type. Rules are evaluated in order; the first rule whose ContentType prefix matches wins. If no rule matches and DefaultValue/DefaultExpires is non-empty, it is used. When the handler already sets a Cache-Control or Expires header, the middleware does not overwrite the respective header.

It returns ErrNoCacheControlRules if Rules is empty.

func CanonicalHostMiddleware added in v0.7.0

func CanonicalHostMiddleware(cfg CanonicalHostConfig) (mux.MiddlewareFunc, error)

CanonicalHostMiddleware returns a middleware that redirects requests to the canonical host when the incoming request scheme or host does not match. The request path and query string are preserved.

This is useful for enforcing a single canonical URL (e.g. redirecting example.com to www.example.com, or HTTP to HTTPS).

func CompressionMiddleware added in v0.2.0

func CompressionMiddleware(cfg CompressionConfig) (mux.MiddlewareFunc, error)

CompressionMiddleware returns a middleware that compresses response bodies using gzip or deflate when the client advertises support via the Accept-Encoding header. Gzip is preferred over deflate when the client accepts both. It uses sync.Pool instances to reuse writers for performance.

Compression is skipped when:

  • The request does not include "gzip" or "deflate" in Accept-Encoding
  • The response already has a Content-Encoding header
  • The response Content-Type is an inherently compressed format (image/*, video/*, audio/*, or common archive types)

It returns ErrInvalidCompressionLevel if Level is outside the valid range.

func ContentNegotiationMiddleware added in v0.7.0

func ContentNegotiationMiddleware(cfg ContentNegotiationConfig) mux.MiddlewareFunc

ContentNegotiationMiddleware returns a middleware that performs proactive content negotiation per RFC 9110 Section 12.5.1. It parses the Accept header, selects the best matching type from the offered list, and stores the result in the request context (retrievable via NegotiatedType).

When Offered is empty, any media type is accepted: the highest quality type from the Accept header is stored in context, and requests always pass through.

When the Accept header is absent or empty, the first offered type is selected per RFC 9110 Section 12.5.1 ("A request without any Accept header field implies that the user agent will accept any media type").

When no offered type matches the Accept header, the middleware responds with 406 Not Acceptable per RFC 9110 Section 15.5.7.

func ContentTypeCheckMiddleware added in v0.2.0

func ContentTypeCheckMiddleware(cfg ContentTypeCheckConfig) (mux.MiddlewareFunc, error)

ContentTypeCheckMiddleware returns a middleware that validates the Content-Type header on requests with matching methods. It returns 415 Unsupported Media Type when the Content-Type is missing or does not match any of the allowed types.

It returns ErrNoAllowedTypes if AllowedTypes is empty.

func EarlyHintsMiddleware added in v0.7.0

func EarlyHintsMiddleware(cfg EarlyHintsConfig) (mux.MiddlewareFunc, error)

EarlyHintsMiddleware returns a middleware that sends a 103 Early Hints informational response per RFC 8297 before the final response. This allows clients to begin preloading resources (stylesheets, scripts, fonts) while the server is still processing the request.

The middleware sets the configured Link headers and writes a 103 status code. The downstream handler then writes the final response as usual. Link headers from the 103 response are not carried over to the final response.

It returns ErrNoLinks if both Links is empty and LinksFunc is nil.

func GenerateUUIDv4 added in v0.2.0

func GenerateUUIDv4(_ *http.Request) string

GenerateUUIDv4 returns a new UUID v4 string.

Spec reference: https://www.rfc-editor.org/rfc/rfc9562#section-5.4

func GenerateUUIDv7 added in v0.2.0

func GenerateUUIDv7(_ *http.Request) string

GenerateUUIDv7 returns a new UUID v7 string. UUIDs are time-ordered: IDs generated later sort lexicographically after earlier ones.

Spec reference: https://www.rfc-editor.org/rfc/rfc9562#section-5.7

func HTCPCPMiddleware added in v0.15.0

func HTCPCPMiddleware(cfg HTCPCPConfig) mux.MiddlewareFunc

HTCPCPMiddleware implements the Hyper Text Coffee Pot Control Protocol (RFC 2324) extended for tea (RFC 7168). It intercepts BREW and WHEN requests and responds according to the configured pot type. All other methods pass through to the next handler.

Behavior summary:

  • Teapot receiving BREW with a coffee variety: 418 I'm a Teapot (RFC 7168 Section 2.3.3).
  • Teapot receiving BREW with a supported tea variety: 200 OK with Content-Type: message/teapot.
  • Coffee pot receiving BREW with a tea variety: 406 Not Acceptable.
  • Either pot receiving BREW while Empty is true: 503 Service Unavailable + Retry-After.
  • WHEN: 200 OK acknowledging the pour stop (RFC 2324 Section 2.1.2).
  • BREW asking for an addition not listed in AvailableAdditions: 406 Not Acceptable.

func IPAllowMiddleware added in v0.6.0

func IPAllowMiddleware(cfg IPAllowConfig) (mux.MiddlewareFunc, error)

IPAllowMiddleware returns a middleware that restricts access to requests originating from the configured IP addresses and CIDR ranges. Requests from IPs not in the allowed list are rejected. The client IP is extracted from r.RemoteAddr.

func IdempotencyMiddleware added in v0.7.0

func IdempotencyMiddleware(cfg IdempotencyConfig) (mux.MiddlewareFunc, error)

IdempotencyMiddleware returns a middleware that caches responses keyed by the Idempotency-Key header per draft-ietf-httpapi-idempotency-key-header. When a request carries a key that has been seen before, the cached response is replayed without invoking the downstream handler. The cached response includes the Idempotency-Key header in the replay.

It returns ErrNoIdempotencyStore if Store is nil.

func IsAprilFirst added in v0.15.0

func IsAprilFirst(t time.Time) bool

IsAprilFirst reports whether the given instant falls on April 1 in its location. Used as the default ActiveOn predicate so HTCPCP-TEA (RFC 2324 published 1 April 1998, extended by RFC 7168 on 1 April 2014) only activates on its anniversary.

func MaintenanceModeMiddleware added in v0.15.0

func MaintenanceModeMiddleware(router *mux.Router, cfg MaintenanceConfig) mux.MiddlewareFunc

MaintenanceModeMiddleware short-circuits matching requests with a "service unavailable" response while a maintenance window is active. The Enabled predicate is the single source of truth; callers back it with whatever they like (atomic.Bool, file presence, env var, cron window) and the middleware reads it per request.

When Enabled returns true and Bypass does not, the middleware sets Retry-After (if configured) and either invokes Response, when set, or writes a default plain-text body with StatusCode. When Enabled returns false, or Bypass returns true, the request flows through to the next handler unchanged.

The router is accepted so the Bypass predicate can resolve route metadata. Pass the same *mux.Router the middleware is attached to via Use.

func MethodOverrideMiddleware added in v0.2.0

func MethodOverrideMiddleware(cfg MethodOverrideConfig) (mux.MiddlewareFunc, error)

MethodOverrideMiddleware returns a middleware that allows clients to override the HTTP method via a configurable header. The first non-empty header value from HeaderNames is uppercased and checked against the allowed set. When allowed, r.Method is set to the override value and the header is removed. Override is only applied when the original request method is in OriginalMethods (defaults to POST).

It returns ErrInvalidOverrideMethod if AllowedMethods or OriginalMethods contains an invalid method.

func NegotiatedType added in v0.7.0

func NegotiatedType(r *http.Request) string

NegotiatedType returns the content type selected by ContentNegotiationMiddleware from the request context. Returns an empty string if no negotiation was performed.

func NoCacheMiddleware added in v0.15.0

func NoCacheMiddleware(router *mux.Router, cfg NoCacheConfig) mux.MiddlewareFunc

NoCacheMiddleware forces responses to be uncacheable. It rewrites caching headers on the response writer at the moment the handler flushes its status line, overriding any Cache-Control, Pragma, or Expires the handler may have set, and removes ETag and Last-Modified so downstream caches cannot perform conditional revalidation.

The Modern preset emits Cache-Control: no-store per RFC 9111 Section 5.2.2.5; Strict adds the legacy Pragma and Expires header combo expected by HTTP/1.0-era intermediaries.

The router argument is accepted so the Skip predicate can resolve route metadata. Pass the same *mux.Router the middleware is attached to via Use.

func PatchContentType added in v0.7.0

func PatchContentType(r *http.Request) string

PatchContentType returns the patch content type stored in the request context by PatchRoutingMiddleware. Returns an empty string for non-PATCH requests or when the middleware is not applied.

func PatchRoutingMiddleware added in v0.7.0

func PatchRoutingMiddleware(cfg PatchRoutingConfig) mux.MiddlewareFunc

PatchRoutingMiddleware returns a middleware that validates the Content-Type of PATCH requests against a set of allowed patch formats and stores the resolved type in the request context. Non-PATCH requests pass through unchanged.

The resolved type is retrievable via PatchContentType.

Returns 415 Unsupported Media Type when the Content-Type is missing or does not match any allowed type.

Spec references:

func ProxyHeadersMiddleware

func ProxyHeadersMiddleware(cfg ProxyHeadersConfig) (mux.MiddlewareFunc, error)

ProxyHeadersMiddleware returns a middleware that populates request fields from reverse proxy headers when the request originates from a trusted proxy.

Supported headers (checked in priority order):

  • r.RemoteAddr: X-Forwarded-For > X-Real-IP [> Forwarded for=]
  • r.URL.Scheme: X-Forwarded-Proto > X-Forwarded-Scheme [> Forwarded proto=]
  • r.Host: X-Forwarded-Host [> Forwarded host=]
  • X-Forwarded-By header: [Forwarded by=]

Bracketed entries require EnableForwarded (RFC 7239). The by= directive is exposed as a synthetic X-Forwarded-By request header.

When TrustedProxies is empty, DefaultTrustedProxies (private RFC 1918/4193 and loopback ranges) is used.

It returns an error if the configuration contains unparseable IP/CIDR entries.

func RecoveryMiddleware added in v0.2.0

func RecoveryMiddleware(cfg RecoveryConfig) mux.MiddlewareFunc

RecoveryMiddleware returns a middleware that recovers from panics in downstream handlers. When a panic occurs it returns 500 Internal Server Error to the client and optionally invokes LogFunc.

func RedirectMiddleware added in v0.8.0

func RedirectMiddleware(cfg RedirectConfig) (mux.MiddlewareFunc, error)

RedirectMiddleware returns a middleware that redirects requests based on path matching rules. It supports exact path matching and prefix matching with a trailing wildcard ("*"). Non-matching requests are passed through to the next handler.

The redirect response includes a standard Location header and an HTML body with a <meta http-equiv="refresh"> tag for clients that do not follow the Location header automatically.

func RegisterProfiler added in v0.8.1

func RegisterProfiler(r *mux.Router)

RegisterProfiler registers the standard net/http/pprof and expvar endpoints on the given router. Mount using Route or PathPrefix:

r.Route("/debug", muxhandlers.RegisterProfiler)
r.Route("/_internal", muxhandlers.RegisterProfiler)

Registered endpoints (relative to the mount path):

/debug/pprof/        - pprof index page
/debug/pprof/cmdline - running program command line
/debug/pprof/profile - CPU profile (supports ?seconds=N)
/debug/pprof/symbol  - symbol lookup
/debug/pprof/trace   - execution trace (supports ?seconds=N)
/debug/vars          - exported variables via the expvar package

Named profiles (allocs, block, goroutine, heap, mutex, threadcreate) are served by the index handler.

See: https://pkg.go.dev/net/http/pprof See: https://pkg.go.dev/expvar

func RequestIDFromContext added in v0.2.0

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request ID stored in the context by RequestIDMiddleware. Returns an empty string if no ID is present.

func RequestIDMiddleware added in v0.2.0

func RequestIDMiddleware(cfg RequestIDConfig) mux.MiddlewareFunc

RequestIDMiddleware returns a middleware that generates or propagates a request ID header. The ID is set on both the request (for downstream handlers) and the response (for the caller).

func RequestSizeLimitMiddleware added in v0.2.0

func RequestSizeLimitMiddleware(cfg RequestSizeLimitConfig) (mux.MiddlewareFunc, error)

RequestSizeLimitMiddleware returns a middleware that limits the size of incoming request bodies. It wraps r.Body with http.MaxBytesReader so that downstream handlers receive an error when reading beyond the limit. The standard http.MaxBytesReader returns 413 Request Entity Too Large automatically when the limit is exceeded.

It returns ErrInvalidMaxSize if MaxBytes is not greater than zero.

func SecurityHeadersMiddleware added in v0.2.0

func SecurityHeadersMiddleware(cfg SecurityHeadersConfig) (mux.MiddlewareFunc, error)

SecurityHeadersMiddleware returns a middleware that sets common security response headers. Headers are set before calling the next handler.

It returns ErrInvalidFrameOption if FrameOption is set to a value other than "DENY", "SAMEORIGIN", or empty string.

func ServerMiddleware added in v0.2.0

func ServerMiddleware(cfg ServerConfig) (mux.MiddlewareFunc, error)

ServerMiddleware returns a middleware that sets server identification response headers. The hostname is resolved once when the middleware is created. It returns an error if the hostname cannot be determined.

func StaticFilesHandler added in v0.2.0

func StaticFilesHandler(cfg StaticFilesConfig) (http.Handler, error)

StaticFilesHandler returns an http.Handler that serves static files from the provided file system. It is not middleware — it serves files directly without calling a next handler.

func SunsetMiddleware added in v0.6.0

func SunsetMiddleware(cfg SunsetConfig) (mux.MiddlewareFunc, error)

SunsetMiddleware returns a middleware that sets the Sunset response header per RFC 8594. Optionally sets the Deprecation and Link headers.

See: https://www.rfc-editor.org/rfc/rfc8594

func TimeoutMiddleware added in v0.2.0

func TimeoutMiddleware(cfg TimeoutConfig) (mux.MiddlewareFunc, error)

TimeoutMiddleware returns a middleware that limits handler execution time. It wraps the handler with http.TimeoutHandler, which returns 503 Service Unavailable when the handler does not complete within the configured duration.

It returns ErrInvalidTimeout if Duration is not greater than zero.

func WriteProblemDetails added in v0.7.0

func WriteProblemDetails(w http.ResponseWriter, problem ProblemDetails)

WriteProblemDetails writes an RFC 9457 Problem Details JSON response. It sets Content-Type to "application/problem+json" and writes the status code from the ProblemDetails struct. If encoding fails, an HTTP 500 Internal Server Error is written instead.

Types

type AcceptPatchConfig added in v0.7.0

type AcceptPatchConfig struct {
	// AcceptPatchTypes is the list of Content-Type values advertised in
	// the Accept-Patch response header for OPTIONS requests. When nil,
	// defaults to application/json, application/merge-patch+json,
	// and application/json-patch+json.
	AcceptPatchTypes []string

	// StatusCode is the HTTP status code for OPTIONS responses.
	// Defaults to 204 No Content.
	StatusCode int
}

AcceptPatchConfig configures the Accept-Patch middleware.

type AccessLogConfig added in v0.15.0

type AccessLogConfig struct {
	// Logger is the slog logger used when LogFunc is nil. It may be a
	// fully pre-configured logger: the middleware inherits whatever
	// handler, output sink, format, level, and pre-bound attributes
	// the caller has set (via slog.New, Logger.With, Logger.WithGroup,
	// etc.). Per-request access-log fields are appended to every
	// emitted record alongside those inherited attributes.
	// Defaults to slog.Default() when both Logger and LogFunc are nil.
	Logger *slog.Logger

	// LogFunc, when non-nil, fully takes over emission: the middleware
	// builds an AccessLogEntry and hands it to LogFunc instead of
	// touching Logger. Use this to integrate with non-slog sinks or to
	// suppress logging conditionally.
	LogFunc func(*AccessLogEntry)

	// Skip, when non-nil, is consulted before the handler runs; if it
	// returns true the request is processed without any log emission.
	// The first argument is the router this middleware was attached
	// to, so callers can resolve the matched route or its metadata
	// (e.g. mux.CurrentRoute(r).GetMetadataValueOr("skip_log", false))
	// to decide. Use it to silence health checks, metrics scrapes, or
	// routes tagged via metadata.
	Skip func(*mux.Router, *http.Request) bool

	// IncludeHeaders lists request header names to record into
	// AccessLogEntry.Headers. Names are matched case-insensitively.
	// When nil, no headers are captured.
	IncludeHeaders []string

	// RedactHeaders lists header names whose values are replaced with
	// "[REDACTED]" in AccessLogEntry.Headers. Authorization, Cookie,
	// Proxy-Authorization, and Set-Cookie are always redacted in
	// addition to anything supplied here. Names are case-insensitive.
	RedactHeaders []string

	// SlowThreshold, when greater than zero, raises the slog level of
	// requests whose duration exceeds it to Warn. Has no effect on
	// LogFunc, which always receives the full entry regardless of
	// duration.
	SlowThreshold time.Duration

	// Now overrides the clock source used for entry timestamps and
	// duration measurement. Defaults to time.Now. Intended for tests.
	Now func() time.Time
}

AccessLogConfig configures the AccessLog middleware.

type AccessLogEntry added in v0.15.0

type AccessLogEntry struct {
	// Time is when the request started processing.
	Time time.Time

	// Method is the HTTP request method (RFC 9110 Section 9).
	Method string

	// Proto is the request protocol as reported by r.Proto, e.g.
	// "HTTP/1.1", "HTTP/2.0".
	Proto string

	// Scheme is the resolved request scheme, "http" or "https". It
	// uses mux.Scheme(r), which infers https when r.URL.Scheme is set
	// (typically by ProxyHeadersMiddleware from a trusted
	// X-Forwarded-Proto) or when the connection is TLS.
	Scheme string

	// Host is r.Host (the Host header value, post-proxy resolution
	// when ProxyHeadersMiddleware is upstream). Useful for multi-vhost
	// deployments.
	Host string

	// Path is r.URL.Path as observed at handler entry; it reflects any
	// path normalization or rewriting performed by upstream middleware.
	Path string

	// Query is r.URL.RawQuery; empty when no query string was present.
	Query string

	// Status is the HTTP status code written by the handler. Defaults
	// to 200 when the handler completed without calling WriteHeader,
	// matching net/http behavior. Set to 0 when Hijacked is true,
	// because the upgrader writes the response bytes directly to the
	// hijacked connection and the middleware cannot observe them.
	Status int

	// Hijacked is true when the handler hijacked the connection via
	// http.Hijacker. The status code is no longer observable by the
	// middleware once a hijack succeeds (the upgrader writes raw bytes
	// to the underlying net.Conn), so Status is zeroed and downstream
	// consumers should treat the entry as "connection upgraded /
	// handed off" rather than a normal 2xx/4xx/5xx response. For
	// WebSocket upgrades the wire status is typically 101 Switching
	// Protocols.
	Hijacked bool

	// Bytes is the total number of response body bytes written.
	Bytes int64

	// Duration is the wall-clock time spent in the handler chain.
	Duration time.Duration

	// RemoteAddr is r.RemoteAddr after any upstream proxy header
	// resolution. Use ProxyHeadersMiddleware to populate this from
	// trusted forwarded headers.
	RemoteAddr string

	// UserAgent is the request's User-Agent header value.
	UserAgent string

	// Referer is the request's Referer header value (RFC 9110 Section
	// 10.1.3, "Referer" preserves the original misspelling).
	Referer string

	// RouteName is the name set via mux.Route.Name, when the matched
	// route has one. Empty when the route is unnamed or no route was
	// matched.
	RouteName string

	// RequestID is the value returned by RequestIDFromContext, when the
	// request flowed through RequestIDMiddleware. Empty otherwise.
	RequestID string

	// Headers contains the request headers selected by IncludeHeaders,
	// with values for headers in RedactHeaders replaced by "[REDACTED]".
	// Nil when no headers are captured.
	Headers map[string]string

	// Err is set when ErrorFunc detects an application-level error
	// (e.g. 5xx status). Optional and informational.
	Err error
}

AccessLogEntry is the structured record produced for every request the AccessLog middleware observes. It is supplied to a user-provided callback (when LogFunc is set), and is also the source of fields the default slog sink emits.

type BasicAuthConfig

type BasicAuthConfig struct {
	// Realm is the authentication realm sent in the WWW-Authenticate header.
	// Defaults to "Restricted" when empty.
	Realm string

	// ValidateFunc is called to validate credentials dynamically.
	// Takes priority over Credentials when both are set.
	ValidateFunc func(username, password string) bool

	// Credentials is a static map of username -> password pairs.
	// Compared using SHA-256 hashed constant-time comparison to prevent
	// timing attacks, including length-based leaks.
	Credentials map[string]string
}

BasicAuthConfig configures the Basic Auth middleware behaviour.

Spec reference: https://www.rfc-editor.org/rfc/rfc7617

type BearerAuthConfig added in v0.7.0

type BearerAuthConfig struct {
	// Realm is the authentication realm sent in the WWW-Authenticate header.
	// Defaults to "Restricted" when empty.
	Realm string

	// ValidateFunc is called to validate the bearer token.
	// It receives the request and the raw token string.
	// Return true to allow the request, false to reject it.
	ValidateFunc func(r *http.Request, token string) bool
}

BearerAuthConfig configures the Bearer Auth middleware behaviour.

Spec reference: https://www.rfc-editor.org/rfc/rfc6750

type CORSConfig

type CORSConfig struct {
	// AllowedOrigins is a list of exact origin strings, "*" for wildcard,
	// or subdomain wildcard patterns like "https://*.example.com".
	AllowedOrigins []string

	// AllowOriginFunc is an optional dynamic callback invoked when the
	// origin does not match any entry in AllowedOrigins. Return true to allow.
	AllowOriginFunc func(origin string) bool

	// AllowedMethods overrides the set of methods advertised in preflight
	// and actual responses. When empty the middleware auto-discovers methods
	// from the router for the matched route.
	AllowedMethods []string

	// AllowedHeaders lists the headers the client may send in the actual
	// request. When empty the middleware reflects the Access-Control-Request-Headers
	// value from the preflight request. Use "*" to reflect all requested headers.
	AllowedHeaders []string

	// ExposeHeaders lists the headers the browser may expose to client code.
	ExposeHeaders []string

	// AllowCredentials sets Access-Control-Allow-Credentials: true.
	// Per the Fetch Standard, "*" cannot be used as Allow-Origin when
	// credentials are enabled; the middleware returns ErrWildcardCredentials.
	AllowCredentials bool

	// MaxAge is the duration in seconds a preflight result may be cached.
	// Positive values are sent as-is, negative values emit "0", zero omits the header.
	MaxAge int

	// OptionsStatusCode overrides the HTTP status code for preflight responses.
	// When zero (default) the middleware uses 204 No Content.
	OptionsStatusCode int

	// OptionsPassthrough, when true, sets CORS headers on preflight but
	// forwards the request to the next handler instead of terminating the chain.
	OptionsPassthrough bool

	// AllowPrivateNetwork, when true, responds to Access-Control-Request-Private-Network
	// preflight headers with Access-Control-Allow-Private-Network: true.
	// See https://wicg.github.io/private-network-access/
	AllowPrivateNetwork bool
}

CORSConfig configures the CORS middleware behaviour.

Spec references:

type CacheControlConfig added in v0.2.0

type CacheControlConfig struct {
	// Rules is the ordered list of content type rules. The first matching
	// rule wins. Required; at least one must be provided.
	Rules []CacheControlRule

	// DefaultValue is the Cache-Control header value for responses that
	// don't match any rule. When empty, no header is set for unmatched
	// types.
	DefaultValue string

	// DefaultExpires is the duration added to the current time to compute
	// the Expires header for responses that don't match any rule. A zero
	// duration produces a date in the past (epoch). A negative duration
	// means no Expires header is set for unmatched types.
	DefaultExpires time.Duration
}

CacheControlConfig configures the CacheControl middleware behaviour.

type CacheControlRule added in v0.2.0

type CacheControlRule struct {
	// ContentType is a content type prefix to match against the response
	// Content-Type (e.g. "image/", "application/json"). Matching is
	// case-insensitive via strings.HasPrefix on the lowercased value.
	ContentType string

	// Value is the Cache-Control header value to set when this rule
	// matches (e.g. "public, max-age=86400").
	Value string

	// Expires is the duration added to the current time to compute the
	// Expires header value (formatted as HTTP-date per RFC 7231). A zero
	// duration produces a date in the past (epoch), equivalent to
	// "already expired". A negative duration means no Expires header is
	// set for this rule. Positive values produce a future date
	// (e.g. 24*time.Hour sets Expires to 24 hours from now).
	Expires time.Duration
}

CacheControlRule maps a Content-Type prefix to Cache-Control and Expires header values.

type CanonicalHostConfig added in v0.7.0

type CanonicalHostConfig struct {
	// URL is the canonical base URL to redirect to, including scheme
	// and host (e.g. "https://www.example.com"). Required.
	URL string

	// StatusCode is the HTTP redirect status code. Defaults to
	// 301 Moved Permanently.
	StatusCode int
}

CanonicalHostConfig configures the Canonical Host middleware.

type CompressionConfig added in v0.2.0

type CompressionConfig struct {
	// Level is the compression level for both gzip and deflate. When zero,
	// flate.DefaultCompression is used. Must be in
	// [flate.HuffmanOnly, flate.BestCompression] or zero.
	Level int

	// MinLength is the minimum response body size in bytes before compression
	// is applied. When zero, all responses are compressed.
	MinLength int
}

CompressionConfig configures the Compression middleware behaviour.

type ContentNegotiationConfig added in v0.7.0

type ContentNegotiationConfig struct {
	// Offered is the list of media types the server can produce, in
	// preference order. When empty, any media type from the Accept header
	// is accepted and the best match is stored in context.
	// Examples: "application/json", "application/xml", "text/html".
	Offered []string
}

ContentNegotiationConfig configures the Content Negotiation middleware.

Spec reference: https://www.rfc-editor.org/rfc/rfc9110#section-12.5.1

type ContentTypeCheckConfig added in v0.2.0

type ContentTypeCheckConfig struct {
	// AllowedTypes is the set of acceptable Content-Type values.
	// Matching is case-insensitive and ignores parameters
	// (e.g. "application/json" matches "application/json; charset=utf-8").
	// Required; at least one must be provided.
	AllowedTypes []string

	// Methods is the set of HTTP methods that require Content-Type
	// validation. When nil, defaults to POST, PUT, PATCH.
	Methods []string
}

ContentTypeCheckConfig configures the Content-Type Check middleware behaviour.

type Drainer added in v0.15.0

type Drainer struct {
	// contains filtered or unexported fields
}

Drainer is the control surface returned by GracefulShutdownMiddleware. Callers invoke Drain() from a signal handler to start rejecting new requests, then use Wait() to block until in-flight requests have completed (typically just before http.Server.Shutdown).

func GracefulShutdownMiddleware added in v0.15.0

func GracefulShutdownMiddleware(router *mux.Router, cfg GracefulShutdownConfig) (mux.MiddlewareFunc, *Drainer)

GracefulShutdownMiddleware returns a middleware that intercepts requests once Drain has been called and a Drainer the caller uses to trigger and observe the drain. Requests arriving before Drain() flow through unchanged; requests arriving after receive the configured drain response unless Bypass forwards them. In-flight requests are tracked via Drainer.InFlight and waited on via Drainer.Wait.

Pair this with http.Server.Shutdown: call Drainer.Drain on SIGTERM, Drainer.Wait to let active requests complete, then Server.Shutdown to close listeners.

The router is accepted so the Bypass predicate can resolve matched-route metadata. Pass the same *mux.Router the middleware is attached to via Use.

func (*Drainer) Drain added in v0.15.0

func (d *Drainer) Drain()

Drain marks the server as draining. After this call returns, every request that enters the middleware is rejected with the configured drain response unless Bypass forwards it. Idempotent.

func (*Drainer) InFlight added in v0.15.0

func (d *Drainer) InFlight() int64

InFlight returns the number of requests currently inside the middleware chain. Useful for metrics and tests; counts only requests the middleware decided to forward to next (i.e. not drained, not bypassed-without-incrementing).

func (*Drainer) IsDraining added in v0.15.0

func (d *Drainer) IsDraining() bool

IsDraining reports whether Drain has been called.

func (*Drainer) Wait added in v0.15.0

func (d *Drainer) Wait(ctx context.Context) error

Wait blocks until the in-flight count reaches zero or the context is cancelled. Returns nil on a clean drain or ctx.Err() if the deadline fires first. Wait is safe to call before, during, or after Drain; when no requests have ever been observed it returns immediately. The implementation polls inFlight at a 20ms cadence, which is invisible against typical shutdown deadlines and avoids per-request signalling overhead on the hot path.

The typical usage is:

drainer.Drain()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = drainer.Wait(shutdownCtx)
_ = srv.Shutdown(shutdownCtx)

type EarlyHintsConfig added in v0.7.0

type EarlyHintsConfig struct {
	// Links is the list of Link header values to send with the 103 Early
	// Hints response. Each entry should follow the format defined in
	// RFC 8288, e.g. `</style.css>; rel=preload; as=style`.
	Links []string

	// LinksFunc is called per request to compute Link header values
	// dynamically. Its results are sent alongside the static Links list.
	// Either Links or LinksFunc (or both) must be set.
	LinksFunc func(r *http.Request) []string
}

EarlyHintsConfig configures the Early Hints middleware.

Spec reference: https://www.rfc-editor.org/rfc/rfc8297

type GracefulShutdownConfig added in v0.15.0

type GracefulShutdownConfig struct {
	// Bypass, when non-nil, is consulted for every request; returning
	// true forwards the request to the next handler even while the
	// drain is in progress. The router is supplied so the predicate
	// can inspect matched-route metadata. Typical uses: keep k8s
	// liveness/readiness probes and /metrics reachable so the
	// orchestrator can observe the drain.
	Bypass func(*mux.Router, *http.Request) bool

	// Response, when non-nil, fully owns the response written to
	// requests that arrive after Drain() has been called. The
	// middleware sets the default headers (Connection: close,
	// Cache-Control: no-store, optional Retry-After) before invoking
	// the handler so the handler can override them or write its own.
	// StatusCode is ignored when Response is set because the handler
	// controls its own status.
	Response http.Handler

	// StatusCode is the HTTP status code for the default drain
	// response when Response is nil. Defaults to 503 Service
	// Unavailable (RFC 9110 Section 15.6.4), which is the spec-correct
	// signal that the server is intentionally rejecting new work.
	StatusCode int

	// RetryAfter, when greater than zero, emits a Retry-After header
	// in delta-seconds form per RFC 9110 Section 10.2.3 on drain
	// responses. Sub-second values round up to 1. Defaults to no
	// header.
	RetryAfter time.Duration
}

GracefulShutdownConfig configures the GracefulShutdown middleware.

type HTCPCPConfig added in v0.15.0

type HTCPCPConfig struct {
	// PotType selects coffee pot or teapot semantics.
	PotType PotType

	// Teas lists the tea varieties this teapot can brew (RFC 7168
	// Section 2.1.1). Names are matched case-insensitively against the
	// Accept-Additions header. Ignored when PotType is PotCoffee. When
	// nil for a teapot, DefaultTeaVarieties is used.
	Teas []string

	// AvailableAdditions lists the additions (milk-type, syrup-type,
	// sweetener-type, etc., per RFC 2324 Section 2.2.2.1) the pot
	// currently has on hand. Requests asking for an addition outside
	// this set receive 406 Not Acceptable.
	AvailableAdditions []string

	// Empty signals that the pot has nothing to brew. BREW requests
	// receive 503 Service Unavailable with a Retry-After header per
	// RFC 2324 Section 2.3.3.
	Empty bool

	// RetryAfter is the value of the Retry-After header sent with 503
	// responses, in seconds. Defaults to 60.
	RetryAfter int

	// ActiveOn restricts the days on which HTCPCP semantics apply. When
	// the predicate returns false the middleware becomes a no-op and
	// forwards the request to the next handler (which typically yields
	// a 404 or 405 since BREW/WHEN are not real HTTP methods). When nil,
	// IsAprilFirst is used, matching the publication date of RFC 2324.
	// The argument is the time returned by Now.
	ActiveOn func(time.Time) bool

	// Now overrides the clock source used by ActiveOn. Defaults to
	// time.Now. Intended for tests; production code should leave it nil.
	Now func() time.Time
}

HTCPCPConfig configures the HTCPCP-TEA middleware.

type IPAllowConfig added in v0.6.0

type IPAllowConfig struct {
	// Allowed is a list of IP addresses and CIDR ranges that are permitted
	// to access the protected routes. Required; must contain at least one
	// entry. Bare IPs are normalized to /32 (IPv4) or /128 (IPv6).
	// Examples: "10.0.0.1", "192.168.0.0/16", "::1", "fd00::/8"
	Allowed []string

	// DeniedHandler is called when the client IP is not in the allowed
	// list. When nil, a default handler returns 403 Forbidden with an
	// empty body.
	DeniedHandler http.Handler
}

IPAllowConfig configures the IP allow middleware.

type IdempotencyConfig added in v0.7.0

type IdempotencyConfig struct {
	// Store is the backing store for cached responses. Required.
	Store IdempotencyStore

	// HeaderName overrides the header used to carry the idempotency key.
	// Defaults to "Idempotency-Key".
	HeaderName string

	// TTL is the time-to-live for cached responses. Defaults to 24 hours.
	// A zero value means entries do not expire.
	TTL time.Duration

	// Methods is the set of HTTP methods that require an idempotency key.
	// When nil, defaults to POST.
	Methods []string

	// EnforceKey, when true, returns 400 Bad Request if the idempotency
	// key header is missing on a matched method. When false (default),
	// requests without the header are passed through without caching.
	EnforceKey bool

	// CacheableStatusCodes is an optional allow list of HTTP status codes
	// that should be cached. When nil, all status codes are cached.
	// When set, only responses with a status code in this list are stored;
	// other responses (e.g. 500) are passed through without caching.
	CacheableStatusCodes []int

	// CacheKeyFunc is an optional function that builds the final cache key
	// from the request and the raw idempotency key header value. Use this
	// to scope cached responses per authenticated user, tenant, or other
	// request-scoped identity. When nil, the default scoping
	// (method + path + header value) is used.
	CacheKeyFunc func(r *http.Request, key string) string

	// ValidateKeyFunc is an optional function to validate the idempotency key
	// format. When set, the middleware calls it before looking up the cache.
	// It receives the request and the raw key value. Return true to accept
	// the key, false to reject it with 400 Bad Request.
	ValidateKeyFunc func(r *http.Request, key string) bool

	// KeyMaxLength is the maximum allowed length of the idempotency key.
	// Keys exceeding this length are rejected with 400 Bad Request.
	// Defaults to 64. Set to -1 for no limit.
	KeyMaxLength int

	// CanCache is an optional pre-check called before any cache lookup or
	// storage. When it returns false, the request is passed through to the
	// handler without idempotency caching. Use this to skip caching based
	// on request properties (e.g. specific paths, headers, or auth state).
	// When nil, all matched requests are eligible for caching.
	CanCache func(r *http.Request) bool

	// OnCacheHit is called when a cached response is found for the
	// idempotency key. Use this for observability (e.g. Prometheus counters).
	// When nil, no callback is invoked.
	OnCacheHit func(r *http.Request, key string)

	// OnCacheMiss is called when no cached response is found and the
	// handler is invoked. Use this for observability (e.g. Prometheus counters).
	// When nil, no callback is invoked.
	OnCacheMiss func(r *http.Request, key string)

	// Locker is an optional distributed lock for in-flight requests.
	// When set, the middleware acquires a lock before invoking the handler
	// and releases it after the response is stored. If the lock cannot be
	// acquired (another request with the same key is in progress), the
	// middleware returns 409 Conflict. When nil, no locking is performed.
	Locker IdempotencyLocker

	// FingerprintFunc is an optional function that computes a fingerprint
	// from the request. The fingerprint is stored alongside the cached
	// response. On cache hit, if the current request's fingerprint does
	// not match the stored one, the middleware returns 422 Unprocessable
	// Entity instead of replaying the cached response. This prevents
	// clients from reusing idempotency keys across different operations.
	// When nil, no fingerprint matching is performed.
	FingerprintFunc func(r *http.Request) string

	// OnConflict is called when a 409 Conflict is returned because the
	// Locker could not acquire a lock (another request with the same
	// key is in-flight). Use this for observability.
	// When nil, no callback is invoked.
	OnConflict func(r *http.Request, key string)

	// OnFingerprintMismatch is called when a 422 Unprocessable Entity is
	// returned because the request fingerprint does not match the cached
	// one. Use this for observability and alerting on key misuse.
	// When nil, no callback is invoked.
	OnFingerprintMismatch func(r *http.Request, key string)

	// RetryAfter is the duration sent in the Retry-After header (as whole
	// seconds) when a 409 Conflict response is returned due to an in-flight
	// lock. When zero, no Retry-After header is sent.
	RetryAfter time.Duration

	// ReplayedHeaderName sets a response header to "true" when a cached
	// response is replayed. Use this to let clients distinguish original
	// responses from replays. When empty, no replay indicator header is
	// set. Example: "X-Idempotency-Replayed".
	ReplayedHeaderName string

	// ErrorHandler is an optional function that writes error responses
	// for all middleware-generated errors (400, 409, 422). When set, it
	// replaces the default http.Error plain-text responses. Use this to
	// return structured JSON errors or RFC 9457 Problem Details.
	// When nil, http.Error is used.
	ErrorHandler func(w http.ResponseWriter, r *http.Request, statusCode int)

	// OnStore is called when a response is successfully stored in the
	// cache. Use this for observability to track cache fill rate. Not
	// called when the response status code is excluded by
	// CacheableStatusCodes. When nil, no callback is invoked.
	OnStore func(r *http.Request, key string, statusCode int)

	// ResponseHeadersFunc is called before writing any response (original,
	// replayed, or error). Use this to inject headers like X-Cache-Age or
	// update the Date header. The replayed parameter is true when the
	// response is a cached replay. When nil, no callback is invoked.
	ResponseHeadersFunc func(w http.Header, r *http.Request, replayed bool)

	// MaxCacheBodySize is the maximum response body size in bytes that
	// will be cached. Responses with bodies exceeding this limit are
	// served to the client but not stored in the cache. When zero, no
	// limit is applied.
	MaxCacheBodySize int64
}

IdempotencyConfig configures the Idempotency middleware.

Spec reference: https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/

type IdempotencyLocker added in v0.7.0

type IdempotencyLocker interface {
	// Lock attempts to acquire a lock for the given key. Returns true if
	// the lock was acquired, false if the key is already locked.
	Lock(ctx context.Context, key string) bool

	// Unlock releases the lock for the given key.
	Unlock(ctx context.Context, key string)
}

IdempotencyLocker is an optional interface for distributed locking of in-flight requests. When a lock cannot be acquired (another request with the same key is in progress), the middleware returns 409 Conflict. Implementations must be safe for concurrent use.

type IdempotencyStore added in v0.7.0

type IdempotencyStore interface {
	// Get retrieves a cached response by key. Returns the serialized
	// response and true if found, or nil and false if not cached.
	Get(ctx context.Context, key string) ([]byte, bool)

	// Set stores a serialized response with the given key and TTL.
	// A zero TTL means the entry does not expire.
	Set(ctx context.Context, key string, value []byte, ttl time.Duration)
}

IdempotencyStore is the interface for storing and retrieving cached responses keyed by idempotency key. Implementations must be safe for concurrent use.

type MaintenanceConfig added in v0.15.0

type MaintenanceConfig struct {
	// Enabled reports whether the maintenance response should be sent
	// for the current request. The predicate is consulted on every
	// request, so callers can flip maintenance on and off by mutating
	// the data structure (atomic.Bool, file, env, scheduled window)
	// the predicate reads. When nil, the middleware is a no-op and
	// every request passes through.
	Enabled func(*http.Request) bool

	// Bypass, when non-nil, is consulted before Enabled is checked; if
	// it returns true the request bypasses maintenance entirely. The
	// router is supplied so the predicate can inspect matched-route
	// metadata, allow lists, header tokens, etc. Typical uses: admin
	// IP allow-list, internal health checks, deploy-pipeline tooling.
	Bypass func(*mux.Router, *http.Request) bool

	// Response, when non-nil, fully owns the maintenance response
	// body. The middleware sets Retry-After (when configured) and then
	// invokes the handler; StatusCode is ignored because the handler
	// controls its own status. Use this to render an HTML page, return
	// RFC 9457 ProblemDetails JSON, redirect to a static maintenance
	// page, or anything else.
	Response http.Handler

	// StatusCode is the HTTP status code for the default response when
	// Response is nil. Defaults to 503 Service Unavailable (RFC 9110
	// Section 15.6.4), which is the spec-correct signal for scheduled
	// maintenance; other codes are an escape hatch and should be used
	// with a clear reason.
	StatusCode int

	// RetryAfter, when greater than zero and RetryAt is the zero value,
	// emits a Retry-After header in delta-seconds form per RFC 9110
	// Section 10.2.3. Sub-second values are rounded down.
	RetryAfter time.Duration

	// RetryAt, when non-zero, emits a Retry-After header in HTTP-date
	// form per RFC 9110 Section 10.2.3, overriding RetryAfter. Use
	// when maintenance has a scheduled end time. Times are formatted
	// in UTC.
	RetryAt time.Time
}

MaintenanceConfig configures the MaintenanceMode middleware.

type MethodOverrideConfig added in v0.2.0

type MethodOverrideConfig struct {
	// HeaderNames is the list of header names checked in order.
	// The first non-empty header value is used as the override.
	// When nil, defaults to
	// ["X-HTTP-Method-Override", "X-Method-Override", "X-HTTP-Method"].
	HeaderNames []string

	// OriginalMethods is the set of HTTP methods eligible for override.
	// When nil, defaults to [POST].
	OriginalMethods []string

	// AllowedMethods restricts which methods can be used as overrides.
	// When nil, defaults to PUT, PATCH, DELETE, HEAD, OPTIONS.
	AllowedMethods []string
}

MethodOverrideConfig configures the Method Override middleware behaviour.

type NoCacheConfig added in v0.15.0

type NoCacheConfig struct {
	// Preset selects which header set the middleware emits. Defaults to
	// NoCachePresetModern (Cache-Control: no-store only).
	Preset NoCachePreset

	// Skip, when non-nil, is consulted before each response is flushed;
	// returning true forwards the handler's response unchanged, leaving
	// any handler-set caching headers intact. The router is supplied so
	// callers can inspect matched-route metadata (e.g. opt specific
	// routes out of the no-cache policy).
	Skip func(*mux.Router, *http.Request) bool
}

NoCacheConfig configures the NoCache middleware.

type NoCachePreset added in v0.15.0

type NoCachePreset int

NoCachePreset selects the set of response headers NoCacheMiddleware writes on each response.

const (
	// NoCachePresetModern emits a single Cache-Control: no-store header
	// per RFC 9111 Section 5.2.2.5, which instructs shared and private
	// caches not to store any part of the response. Sufficient for any
	// client and intermediary that respects RFC 9111.
	NoCachePresetModern NoCachePreset = iota

	// NoCachePresetStrict emits the legacy "no-cache combo" expected by
	// caches and clients predating RFC 7234 / RFC 9111:
	//
	//   Cache-Control: no-store, no-cache, must-revalidate, max-age=0,
	//                  private
	//   Pragma:        no-cache
	//   Expires:       0
	//
	// Pragma comes from RFC 1945; Expires: 0 is the HTTP/1.0 convention
	// for "already expired". Use when downstream caches may be ancient
	// or non-conformant.
	NoCachePresetStrict
)

type PatchRoutingConfig added in v0.7.0

type PatchRoutingConfig struct {
	// AllowedTypes is the set of accepted Content-Type values for PATCH
	// requests. Matching is case-insensitive and ignores parameters.
	// When nil, defaults to application/json, application/merge-patch+json,
	// and application/json-patch+json.
	AllowedTypes []string
}

PatchRoutingConfig configures the Patch Routing middleware.

type PotType added in v0.15.0

type PotType int

PotType identifies the kind of pot the middleware represents.

const (
	// PotCoffee is a coffee pot. Brews coffee; refuses tea per RFC 7168
	// Section 2.1.1 (only teapots brew tea).
	PotCoffee PotType = iota

	// PotTeapot is a teapot. Brews tea; refuses coffee with 418 per
	// RFC 2324 Section 2.3.2 and RFC 7168 Section 2.3.3.
	PotTeapot
)

type ProblemDetails added in v0.7.0

type ProblemDetails struct {
	// Type is a URI reference that identifies the problem type. When
	// dereferenced, it should provide human-readable documentation.
	// Defaults to "about:blank" when empty, per RFC 9457 Section 3.1.3.
	Type string `json:"type"`

	// Title is a short, human-readable summary of the problem type.
	// It should not change from occurrence to occurrence of the same
	// problem type, per RFC 9457 Section 3.1.4.
	Title string `json:"title"`

	// Status is the HTTP status code for this occurrence of the problem.
	// Per RFC 9457 Section 3.1.1.
	Status int `json:"status"`

	// Detail is a human-readable explanation specific to this occurrence
	// of the problem, per RFC 9457 Section 3.1.5.
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference that identifies the specific occurrence
	// of the problem, per RFC 9457 Section 3.1.2.
	Instance string `json:"instance,omitempty"`

	// Extensions contains additional members beyond the standard fields.
	// Per RFC 9457 Section 3.2, problem types may extend the object with
	// additional members that provide further context.
	Extensions map[string]any `json:"-"`
}

ProblemDetails represents an RFC 9457 Problem Details object.

Spec reference: https://www.rfc-editor.org/rfc/rfc9457

func NewProblemDetails added in v0.7.0

func NewProblemDetails(status int) ProblemDetails

NewProblemDetails creates a ProblemDetails with the given status code and the standard status text as title. Type defaults to "about:blank" per RFC 9457 Section 4.2.

func (ProblemDetails) MarshalJSON added in v0.7.0

func (p ProblemDetails) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. It serializes the standard fields and merges any extension members into the top-level JSON object, per RFC 9457 Section 3.2.

type ProxyHeadersConfig

type ProxyHeadersConfig struct {
	// TrustedProxies is a list of IP addresses and CIDR ranges.
	// Forwarding headers are only honoured when r.RemoteAddr is in this set.
	// When empty, DefaultTrustedProxies (private/loopback ranges) is used.
	// Examples: "10.0.0.1", "192.168.0.0/16", "::1", "fd00::/8"
	TrustedProxies []string

	// EnableForwarded enables parsing of the RFC 7239 Forwarded header.
	// When enabled, the Forwarded header is used as a fallback after the
	// de-facto X-Forwarded-* and X-Real-IP headers.
	//
	// Spec reference: https://www.rfc-editor.org/rfc/rfc7239
	EnableForwarded bool
}

ProxyHeadersConfig configures the ProxyHeaders middleware behaviour.

type RecoveryConfig added in v0.2.0

type RecoveryConfig struct {
	// LogFunc is an optional callback invoked with the request and the
	// recovered value when a panic occurs. When nil, no logging is performed.
	LogFunc func(r *http.Request, err any)
}

RecoveryConfig configures the Recovery middleware behaviour.

type RedirectConfig added in v0.8.0

type RedirectConfig struct {
	// Rules is the list of redirect rules evaluated in order.
	// The first matching rule wins.
	Rules []RedirectRule

	// StatusCode is the default HTTP redirect status code.
	// Defaults to 307 Temporary Redirect.
	StatusCode int
}

RedirectConfig configures the Redirect middleware.

type RedirectRule added in v0.8.0

type RedirectRule struct {
	// From is the path to match. Must start with "/".
	// A trailing "*" enables prefix matching: "/old/*" matches any path
	// starting with "/old/" and appends the remainder to To.
	// Without "*", only exact path matches trigger a redirect.
	From string

	// To is the redirect target. For prefix rules, the matched suffix
	// is appended. Can be an absolute URL for external redirects.
	To string

	// StatusCode is the HTTP redirect status code for this rule.
	// Overrides the default from RedirectConfig. If zero, the config
	// default is used.
	StatusCode int
}

RedirectRule defines a single redirect mapping.

type RequestIDConfig added in v0.2.0

type RequestIDConfig struct {
	// HeaderName overrides the header used to propagate the request ID.
	// Defaults to "X-Request-ID" when empty.
	HeaderName string

	// GenerateFunc is an optional callback that returns a new unique ID.
	// It receives the current request, allowing ID generation based on
	// request context. Defaults to GenerateUUIDv4.
	GenerateFunc func(r *http.Request) string

	// TrustIncoming, when true, reuses an existing request ID from the
	// incoming request header instead of generating a new one.
	TrustIncoming bool
}

RequestIDConfig configures the Request ID middleware behaviour.

type RequestSizeLimitConfig added in v0.2.0

type RequestSizeLimitConfig struct {
	// MaxBytes is the maximum allowed request body size in bytes.
	// Must be greater than zero.
	MaxBytes int64
}

RequestSizeLimitConfig configures the Request Size Limit middleware behaviour.

type SecurityHeadersConfig added in v0.2.0

type SecurityHeadersConfig struct {
	// DisableContentTypeNosniff disables the X-Content-Type-Options: nosniff
	// header. The header is set by default (when false).
	DisableContentTypeNosniff bool

	// FrameOption sets the X-Frame-Options header value.
	// Valid values are "DENY", "SAMEORIGIN", or empty string to skip.
	// Defaults to "DENY".
	FrameOption string

	// ReferrerPolicy sets the Referrer-Policy header value.
	// Defaults to "strict-origin-when-cross-origin".
	ReferrerPolicy string

	// HSTSMaxAge sets the max-age directive for the Strict-Transport-Security
	// header in seconds. When zero, the header is not set.
	HSTSMaxAge int

	// HSTSIncludeSubDomains appends the includeSubDomains directive to the
	// Strict-Transport-Security header. Only effective when HSTSMaxAge > 0.
	HSTSIncludeSubDomains bool

	// HSTSPreload appends the preload directive to the
	// Strict-Transport-Security header. Only effective when HSTSMaxAge > 0.
	HSTSPreload bool

	// CrossOriginOpenerPolicy sets the Cross-Origin-Opener-Policy header.
	// When empty, the header is not set.
	CrossOriginOpenerPolicy string

	// ContentSecurityPolicy sets the Content-Security-Policy header.
	// When empty, the header is not set.
	ContentSecurityPolicy string

	// PermissionsPolicy sets the Permissions-Policy header.
	// When empty, the header is not set.
	PermissionsPolicy string
}

SecurityHeadersConfig configures the Security Headers middleware behaviour.

type ServerConfig added in v0.2.0

type ServerConfig struct {
	// Hostname is the value written to the X-Server-Hostname response
	// header. Resolution order: Hostname field, then HostnameEnv
	// environment variable, then os.Hostname.
	Hostname string

	// HostnameEnv is a list of environment variable names checked in
	// order (e.g. ["POD_NAME", "HOSTNAME"]). The first non-empty
	// value is used. Only consulted when Hostname is empty. When all
	// variables are unset or empty, os.Hostname is used as a fallback.
	HostnameEnv []string
}

ServerConfig configures the Server middleware behaviour.

type StaticFilesConfig added in v0.2.0

type StaticFilesConfig struct {
	// FS is the file system to serve files from. Required.
	// Works with os.DirFS, embed.FS, and any fs.FS implementation.
	FS fs.FS

	// EnableDirectoryListing allows directory contents to be listed
	// when no index.html is present. Disabled by default for security.
	EnableDirectoryListing bool

	// SPAFallback serves the root index.html for any path that does
	// not match an existing file. This allows client-side routers to
	// handle all routes. Requires index.html at the root of FS.
	SPAFallback bool

	// EnableETag precomputes strong ETags for all files by walking the
	// FS at init time. Designed for immutable file systems such as
	// embed.FS. The handler sets the ETag response header and handles
	// If-None-Match conditional requests (304 Not Modified).
	EnableETag bool

	// PathPrefix is the URL path prefix under which the handler is
	// mounted. The handler strips this prefix before looking up files
	// in the FS, replacing the need for http.StripPrefix.
	PathPrefix string

	// Aliases maps URL paths to file paths in the FS. Keys are
	// relative to PathPrefix (the prefix is stripped before matching).
	// Alias targets are validated at init time. ETag support applies
	// to aliased paths.
	//
	// Example:
	//
	//	Aliases: map[string]string{
	//	    "/policy-builder/":    "policy-builder.html",
	//	    "/policy-playground/": "policy-playground.html",
	//	}
	Aliases map[string]string
}

StaticFilesConfig configures the static file handler.

type SunsetConfig added in v0.6.0

type SunsetConfig struct {
	// Sunset is the point in time when the resource is expected to become
	// unresponsive. Serialized as an HTTP-date per RFC 7231 Section 7.1.1.1.
	// Required.
	//
	// See: https://www.rfc-editor.org/rfc/rfc8594#section-3
	Sunset time.Time

	// Deprecation is the point in time when the resource was deprecated.
	// When non-zero, the Deprecation response header is set.
	Deprecation time.Time

	// Link is an optional URI pointing to documentation about the
	// deprecation or sunset. When non-empty, a Link header with
	// rel="sunset" is added to the response.
	//
	// See: https://www.rfc-editor.org/rfc/rfc8594#section-4
	Link string
}

SunsetConfig configures the Sunset middleware.

type TimeoutConfig added in v0.2.0

type TimeoutConfig struct {
	// Duration is the maximum time allowed for the handler to complete.
	// Must be greater than zero.
	Duration time.Duration

	// Message is the response body returned when the handler times out.
	// When empty, the standard library default is used.
	Message string
}

TimeoutConfig configures the Timeout middleware behaviour.

Jump to

Keyboard shortcuts

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