MuxMaster is a high-performance HTTP router for Go. It is 100% compatible with the standard net/http package, requires zero external dependencies, and is built on a radix tree (compressed prefix trie) that delivers O(k) route lookup — where k is the length of the URL path, not the number of registered routes.
The hot path allocates zero bytes for static routes and makes a single tiered allocation (384–480 B) for parameterized routes by default — fusing the request context and parameters in one GC-class-aligned object. With the opt-in Mux.PoolRequestBundle = true, that single allocation is recycled via sync.Pool and the entire hot path becomes zero-allocation — see the Maximum Performance Guide.
- Zero allocations for static routes — parameterized routes use a single tiered allocation that fuses request context and parameters, minimising allocator pressure
- Zero allocations on parameterised routes too (opt-in) —
Mux.PoolRequestBundle = truerecycles the per-request bundle viasync.Pool, dropping 1-param routes to 45 ns / 0 B / 0 allocs (20 % faster thanhttprouter); see Maximum Performance Guide - 100%
net/httpcompatible — drop in anywherehttp.Handleris accepted; works with all existing middleware - Zero external dependencies — pure standard library; no dependency bloat
- Radix tree routing — O(k) lookup, independent of the total number of registered routes
- Path parameters — named (
:id), regex-constrained ({id:[0-9]+}), and catch-all (*filepath) - Typed parameter helpers — parse path parameters directly to
int,int64,float64,bool - Middleware scopes — apply middleware globally, to a group, or to a single route
- Groups and sub-groups — organize routes with shared path prefixes and middleware stacks
- Error-returning handlers —
HandlerFuncEenables centralized error handling without boilerplate - FastHandler routes — ultra-low-latency alternative that bypasses context allocation; params passed as a direct argument
- 14 built-in middleware — logger, CORS, Basic Auth, compression, throttle, timeout, and more
- Route introspection —
Lookup,Routes,Walk, andWalkFastfor programmatic route inspection
- Installation
- Quick Start
- Route Syntax
- Path Parameters
- Middleware
- Fast Routes
- Maximum Performance Mode (Zero Allocations)
- Groups
- Mounting Sub-Routers
- Static Files
- Error Handling
- Response Helpers
- Router Options
- Included Middleware
- Route Introspection
- Benchmarks
- Documentation
- Contributing
- License
go get github.com/FlavioCFOliveira/MuxMaster
Requires Go 1.26 or later.
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/FlavioCFOliveira/MuxMaster"
"github.com/FlavioCFOliveira/MuxMaster/middleware"
)
func main() {
mux := muxmaster.New()
// Global middleware — applied to every route registered below.
mux.Use(middleware.Logger(os.Stdout))
mux.Use(middleware.Recoverer)
mux.GET("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
// Named path parameter
mux.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
id := muxmaster.PathParam(r, "id")
fmt.Fprintf(w, "user %s\n", id)
})
// Versioned API group with its own middleware
api := mux.Group("/api/v1")
api.Use(requireAPIKey)
api.GET("/items", listItems)
api.POST("/items", createItem)
api.DELETE("/items/:id", deleteItem)
log.Fatal(http.ListenAndServe(":8080", r))
}MuxMaster supports four types of path segments:
| Pattern | Example match | Description |
|---|---|---|
/users |
/users |
Static segment — exact match |
/users/:id |
/users/42 |
Named parameter — matches one segment |
/users/{id:[0-9]+} |
/users/42 |
Regex parameter — validates the value |
/files/*filepath |
/files/img/logo.png |
Catch-all — matches the rest of the path |
Rules:
- Path parameters (
:name) match exactly one path segment (no/). - Regex parameters (
{name:pattern}) match one segment and must satisfy the regular expression. - Catch-all parameters (
*name) match the remainder of the path, including slashes. - Parameters are extracted in the order they appear in the pattern.
- Conflicts between static and parameterized segments at the same position resolve in favour of the static route.
mux.GET("/users", listUsers)
mux.POST("/users", createUser)
mux.PUT("/users/:id", updateUser)
mux.PATCH("/users/:id", patchUser)
mux.DELETE("/users/:id", deleteUser)
mux.HEAD("/users/:id", headUser)
mux.OPTIONS("/users", optionsUsers)
// Register a handler for all standard HTTP methods at once
mux.ANY("/health", healthCheck)
// Register a handler for a specific subset of methods
mux.Match([]string{"GET", "HEAD"}, "/ping", pingHandler)
// Low-level registration accepting any http.Handler
mux.Handle("GET", "/users", http.HandlerFunc(listUsers))mux.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
id := muxmaster.PathParam(r, "id")
fmt.Fprintf(w, "user: %s\n", id)
})mux.GET("/posts/:year/:month/:slug", func(w http.ResponseWriter, r *http.Request) {
ps := muxmaster.ParamsFromContext(r.Context())
year := ps.Get("year")
month := ps.Get("month")
slug := ps.Get("slug")
fmt.Fprintf(w, "%s/%s/%s\n", year, month, slug)
})Params provides helpers that parse string values into Go types, returning an error if the parameter is absent or the value cannot be parsed:
mux.GET("/items/:id", func(w http.ResponseWriter, r *http.Request) {
ps := muxmaster.ParamsFromContext(r.Context())
id, err := ps.Int("id")
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
_ = id // int
})| Method | Return type | Notes |
|---|---|---|
ps.Get("name") |
string |
Returns "" if not present |
ps.Lookup("name") |
string, bool |
Returns presence flag |
ps.Int("name") |
int, error |
|
ps.Int64("name") |
int64, error |
|
ps.Uint64("name") |
uint64, error |
|
ps.Float64("name") |
float64, error |
|
ps.Bool("name") |
bool, error |
|
ps.Map() |
map[string]string |
All params as a map |
mux.GET("/files/*filepath", func(w http.ResponseWriter, r *http.Request) {
filepath := muxmaster.PathParam(r, "filepath")
// For /files/img/logo.png, filepath == "/img/logo.png"
fmt.Fprintln(w, filepath)
})// Only matches /users/42, /users/100 — not /users/abc
mux.GET("/users/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
id := muxmaster.PathParam(r, "id")
fmt.Fprintln(w, id)
})Parameters are stored in the request context and are accessible anywhere you have access to the request:
func auditMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ps := muxmaster.ParamsFromContext(r.Context())
log.Printf("params: %v", ps)
next.ServeHTTP(w, r)
})
}Each path parameter is a Param struct with Key and Value string fields:
mux.GET("/posts/:year/:month/:slug", func(w http.ResponseWriter, r *http.Request) {
ps := muxmaster.ParamsFromContext(r.Context())
for _, p := range ps {
fmt.Printf("%s=%s\n", p.Key, p.Value)
}
})RoutePattern returns the registered route pattern that matched the request (e.g. /users/:id), or "" if the request has not been matched yet:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
pattern := muxmaster.RoutePattern(r)
log.Printf("%s %s matched pattern %s", r.Method, r.URL.Path, pattern)
})
}Middleware is a function with the signature func(http.Handler) http.Handler. MuxMaster applies middleware at registration time, so the call to Use must appear before the routes it should wrap.
mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout)) // outermost
mux.Use(middleware.Recoverer) // innermost before the handler
mux.GET("/users", listUsers) // wrapped by both Logger and RecovererPre-routing middleware runs before route matching. This is useful for path rewriting, cleaning, or stripping prefixes before the router sees the URL.
mux.Pre(middleware.CleanPath)
mux.Pre(middleware.StripSlashes)With creates a copy of the current router or group with additional middleware scoped to a single route call:
mux.With(requireAdmin).DELETE("/users/:id", deleteUser)
mux.With(rateLimit, audit).POST("/payments", processPayment)Any function of the form func(http.Handler) http.Handler is valid middleware:
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValid(token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
mux.Use(requireAuth)FastHandler is a high-performance handler type that receives path parameters as a direct argument, bypassing the context.WithValue allocation used by standard http.Handler routes. Use it on latency-sensitive endpoints where every nanosecond matters.
type FastHandler func(http.ResponseWriter, *http.Request, Params)Parameters are valid only for the duration of the handler call. If you spawn a goroutine that outlives the handler, copy the slice first:
func myFast(w http.ResponseWriter, r *http.Request, ps muxmaster.Params) {
ps2 := make(muxmaster.Params, len(ps))
copy(ps2, ps)
go func() { process(ps2) }()
}Convenience methods exist for all standard HTTP verbs:
mux := muxmaster.New()
mux.GETFast("/api/v1/users/:id", func(w http.ResponseWriter, r *http.Request, ps muxmaster.Params) {
id := ps.Get("id")
fmt.Fprintf(w, "user: %s\n", id)
})
mux.POSTFast("/api/v1/items", createItemFast)
mux.DELETEFast("/api/v1/items/:id", deleteItemFast)
// Or use HandleFast for any method
mux.HandleFast("GET", "/files/*filepath", serveFilesFast)Available methods: GETFast, HEADFast, POSTFast, PUTFast, PATCHFast, DELETEFast, OPTIONSFast, CONNECTFast, TRACEFast.
FastHandler routes do not support stdlib middleware (func(http.Handler) http.Handler). Use FastMiddleware instead:
type FastMiddleware func(FastHandler) FastHandler
func loggingFast(next muxmaster.FastHandler) muxmaster.FastHandler {
return func(w http.ResponseWriter, r *http.Request, ps muxmaster.Params) {
log.Printf("%s %s", r.Method, r.URL.Path)
next(w, r, ps)
}
}
mux.UseFast(loggingFast)
mux.GETFast("/api/status", statusFast)UseFast must be called before the fast routes it should wrap, just like Use for standard routes.
Groups support fast routes via HandleFast and fast middleware via UseFast:
api := mux.Group("/api/v1")
api.UseFast(loggingFast)
api.HandleFast("GET", "/users/:id", getUserFast)
api.HandleFast("POST", "/users", createUserFast)Apple M4, Go 1.26.2:
| Type | ns/op | B/op | allocs/op |
|---|---|---|---|
Static http.Handler |
14 ns | 0 B | 0 |
Static FastHandler |
~14 ns | 0 B | 0 |
1-param http.Handler (default) |
57 ns | 384 B | 1 |
1-param FastHandler (default) |
~28 ns | 32 B | 1 |
1-param http.Handler + PoolRequestBundle |
28 ns | 0 B | 0 |
1-param FastHandler + PoolFastParams |
28 ns | 0 B | 0 |
Trade-offs:
- Incompatible with stdlib middleware — use
FastMiddlewareonly - Params are not in the request context — they are passed as a direct argument
Lookup()returnsnilfor FastHandler routes — useWalkFastto enumerate them- No convenience short-hand methods on
Group(usegroup.HandleFast("GET", ...))
For services where every nanosecond and every allocation count, MuxMaster offers two opt-in sync.Pool switches that recycle the per-request objects and bring the routing layer to zero allocations:
mux := muxmaster.New()
mux.PoolRequestBundle = true // 0-alloc Handle path (Opt O13)
mux.PoolFastParams = true // 0-alloc HandleFast path (Opt O9)
mux.GET("/users/:id", getUser) // 28 ns / 0 B / 0 allocs (Apple M4)
mux.GETFast("/health", healthFast) // 28 ns / 0 B / 0 allocs (Apple M4)These switches require a stricter handler lifetime contract: handlers MUST NOT retain *http.Request (or the Params slice on FastHandler) past return. A goroutine that captures r would observe a recycled bundle belonging to a future request — effectively a use-after-free against the pool storage.
Audit checklist, worked recipes, and a runnable example are in the Maximum Performance Guide and examples/max-performance/. With both pools enabled, MuxMaster is the fastest stdlib-compatible HTTP router in the Go ecosystem — 20 % faster than httprouter on 1-param routes with zero allocations.
Groups share a path prefix and an optional middleware stack. All routes registered on a group are prefixed with the group's path and wrapped with the group's middleware (applied after any mux-level middleware).
api := mux.Group("/api/v1")
api.Use(requireAPIKey)
api.GET("/users", listUsers) // matches GET /api/v1/users
api.POST("/users", createUser) // matches POST /api/v1/usersapi := mux.Group("/api/v1")
api.Use(requireAPIKey)
admin := api.Group("/admin")
admin.Use(requireAdmin)
admin.DELETE("/users/:id", deleteUser) // matches DELETE /api/v1/admin/users/:idRoute creates a group and calls a function with it — useful for keeping related routes together:
mux.Route("/api/v1", func(api *muxmaster.Group) {
api.Use(requireAPIKey)
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.Route("/admin", func(admin *muxmaster.Group) {
admin.Use(requireAdmin)
admin.DELETE("/users/:id", deleteUser)
})
})With on a group returns a copy of the group with additional middleware for the next registration only:
api.With(requireAdmin).DELETE("/users/:id", deleteUser)
api.With(throttle).POST("/exports", exportData)Match registers the same handler for a set of HTTP methods:
api := mux.Group("/api/v1")
api.Match([]string{"GET", "HEAD"}, "/status", statusHandler)Mount attaches a separate http.Handler (including another *muxmaster.Mux) at a path prefix. The prefix is stripped before the request is forwarded to the mounted handler.
// Build a versioned sub-router independently
v2 := muxmaster.New()
v2.GET("/items", listItemsV2)
v2.POST("/items", createItemV2)
// Attach it to the main router
mux.Mount("/v2", v2)
// Now GET /v2/items → handled by listItemsV2Mounted handlers receive a request with the prefix stripped from r.URL.Path, so the sub-router sees /items, not /v2/items.
ServeFiles serves files from a http.FileSystem. The route pattern must end with /*name.
// Serve files from the ./public directory
mux.ServeFiles("/static/*filepath", http.Dir("./public"))
// GET /static/css/main.css → ./public/css/main.css
// Serve embedded files (Go 1.16+)
import "embed"
//go:embed public
var publicFS embed.FS
mux.ServeFiles("/assets/*filepath", http.FS(publicFS))ServeFiles protects against directory traversal attacks by delegating to http.FileServer.
HandlerFuncE extends the standard handler signature with an error return value. This eliminates repetitive if err != nil { http.Error(...) } blocks:
mux.GETE("/users/:id", func(w http.ResponseWriter, r *http.Request) error {
id, err := muxmaster.ParamsFromContext(r.Context()).Int("id")
if err != nil {
return muxmaster.Error(http.StatusBadRequest, err)
}
user, err := db.FindUser(id)
if err != nil {
return muxmaster.Error(http.StatusNotFound, err)
}
return muxmaster.JSON(w, http.StatusOK, user)
})The common HTTP methods have error-returning variants: GETE, POSTE, PUTE, PATCHE, DELETEE, HEADE, OPTIONSE. Use HandleE directly for CONNECT and TRACE.
muxmaster.Error(code, err) wraps an error with an HTTP status code. The router's default error handler checks for HTTPError and uses its status code; non-HTTPError errors produce a 500.
// Construct an HTTPError
err := muxmaster.Error(http.StatusNotFound, errors.New("user not found"))Set ErrorHandler to centralize error handling across all HandlerFuncE routes and groups:
mux.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
var he muxmaster.HTTPError
if errors.As(err, &he) {
muxmaster.JSON(w, he.StatusCode(), map[string]string{"error": err.Error()})
return
}
log.Printf("unexpected error: %v", err)
muxmaster.JSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
}mux.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
muxmaster.JSON(w, http.StatusNotFound, map[string]string{
"error": "the requested resource was not found",
})
})
mux.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
muxmaster.JSON(w, http.StatusMethodNotAllowed, map[string]string{
"error": "method not allowed",
})
})PanicHandler intercepts panics in handlers and prevents them from crashing the server:
mux.PanicHandler = func(w http.ResponseWriter, r *http.Request, rcv any) {
log.Printf("panic: %v\n%s", rcv, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}GlobalOPTIONS replaces the default auto-generated response for OPTIONS requests:
mux.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusNoContent)
})By default, when HandleOPTIONS is true, MuxMaster responds with 204 No Content and an Allow header listing all registered methods for the matched path.
MuxMaster provides a small set of response-writing helpers. All functions write the Content-Type header and status code automatically.
// JSON response
muxmaster.JSON(w, http.StatusOK, map[string]any{"id": 42, "name": "Alice"})
// XML response
muxmaster.XML(w, http.StatusOK, struct {
XMLName xml.Name `xml:"user"`
Name string `xml:"name"`
}{Name: "Alice"})
// Plain text response
muxmaster.Text(w, http.StatusOK, "pong")
// Redirect
muxmaster.Redirect(w, r, http.StatusMovedPermanently, "/new-path")
// 204 No Content
muxmaster.NoContent(w)Using JSON in an error-returning handler:
mux.POSTE("/users", func(w http.ResponseWriter, r *http.Request) error {
var payload CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
return muxmaster.Error(http.StatusBadRequest, err)
}
user, err := db.CreateUser(payload)
if err != nil {
return err
}
return muxmaster.JSON(w, http.StatusCreated, user)
})All options are fields on *Mux and can be set after New() and before registering routes or starting the server.
mux := muxmaster.New()
// Automatically redirect /foo/ → /foo when /foo is registered (and vice versa).
// Default: true
mux.RedirectTrailingSlash = true
// Automatically redirect /FOO → /foo when /foo is registered (case-insensitive redirect).
// Default: true
mux.RedirectFixedPath = true
// Return 405 Method Not Allowed (with Allow header) instead of 404 when the path
// is registered but not for the requested method.
// Default: true
mux.HandleMethodNotAllowed = true
// Automatically respond to OPTIONS requests with the allowed methods.
// Default: true
mux.HandleOPTIONS = true
// Match routes case-insensitively (no redirect, just serves the route directly).
// Default: false
mux.CaseInsensitive = false
// Use r.URL.RawPath for route matching instead of r.URL.Path.
// Useful when path values contain encoded slashes (%2F).
// Default: false
mux.UseRawPath = false
// Percent-decode path parameter values before returning them.
// Default: false
mux.UnescapePathValues = false
// HTTP redirect code used by RedirectTrailingSlash and RedirectFixedPath.
// Default: 0 (auto: 301 for GET/HEAD, 307 for all other methods)
mux.RedirectCode = http.StatusMovedPermanently // override to force a specific codeConfiguration flags are frozen on the first ServeHTTP call. To change a flag after the server has started serving, call Rebuild():
mux.RedirectTrailingSlash = false
mux.Rebuild() // resets the frozen config snapshotWarning: do not call Rebuild() while the server is actively serving requests.
The middleware sub-package provides 17 production-ready middleware components. Import it separately:
import "github.com/FlavioCFOliveira/MuxMaster/middleware"| Middleware | Description |
|---|---|
Logger |
Request/response logging (method, path, status, duration) |
Recoverer |
Panic recovery — returns 500 and logs the stack trace |
CORS |
Cross-Origin Resource Sharing with full options |
BasicAuth |
HTTP Basic Authentication |
Compress |
Gzip/deflate response compression |
ThrottleBacklog |
Concurrency limiting with a backlog queue |
Timeout |
Per-request deadline using context.WithTimeout |
RequestID |
Generates and attaches a unique request ID |
RealIP |
Extracts the real client IP from proxy headers |
CleanPath |
Normalizes double slashes and dot segments |
StripSlashes |
Removes trailing slashes before routing |
NoCache |
Sets Cache-Control: no-cache, no-store |
SetHeader |
Sets arbitrary response headers |
WithValue |
Stores a value in the request context |
APIKey |
API key authentication with SHA-256 hashing |
JWTAuth |
JWT Bearer token validation (HS*/RS*/ES*) |
OAuth2Introspect |
RFC 7662 token introspection with caching |
// Structured request logging
mux.Use(middleware.Logger(os.Stdout))
// Panic recovery
mux.Use(middleware.Recoverer)
// CORS for a single-page application
mux.Use(middleware.CORS(middleware.CORSOptions{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 86400,
}))
// HTTP Basic Authentication
mux.Use(middleware.BasicAuth("realm", map[string]string{
"admin": "secret",
}))
// Gzip compression
mux.Use(middleware.Compress(5))
// Limit concurrency to 100 simultaneous requests, queue up to 50, timeout after 30s
mux.Use(middleware.ThrottleBacklog(100, 50, 30*time.Second))
// 10-second request deadline
mux.Use(middleware.Timeout(10 * time.Second))
// Attach a unique X-Request-Id header to every request
mux.Use(middleware.RequestID)
// Trust X-Forwarded-For / X-Real-IP only from a known reverse proxy.
// SECURITY: never call middleware.RealIP() without trusted CIDRs in
// production — every peer would be allowed to spoof these headers
// (TM-2026-044). See "Security defaults" below.
proxyCIDR := netip.MustParsePrefix("10.0.0.0/8")
mux.Use(middleware.RealIP(&proxyCIDR))
// Set a custom response header on every request
mux.Use(middleware.SetHeader("X-Content-Type-Options", "nosniff"))
// Store a value in the request context
mux.Use(middleware.WithValue("env", "production"))Three authentication middleware components cover the most common scenarios.
// Authenticate requests by API key, with identity lookup:
mux.Use(middleware.APIKey(middleware.APIKeyOptions{
Keys: map[string]string{
"sk_test_abc123": "user-123", // raw key → identity
"sk_test_def456": "user-456",
},
Header: "X-API-Key", // default; can be customised
}))
mux.GET("/api/data", func(w http.ResponseWriter, r *http.Request) {
identity, _ := middleware.GetAPIKeyIdentity(r.Context())
fmt.Fprintf(w, "authenticated as %s\n", identity)
})// Validate JWT tokens from the Authorization header.
// RequireExpiry: true rejects tokens without an "exp" claim, per
// RFC 8725 §4.4 — without it, a stolen token is valid forever
// (TM-2026-001). See "Security defaults" below.
pubKey, _ := jwt.ReadFile("public.pem") // *ecdsa.PublicKey or *rsa.PublicKey
mux.Use(middleware.JWTAuth(middleware.JWTOptions{
PublicKey: pubKey,
Algorithms: []string{"ES256"},
Issuers: []string{"https://auth.example.com"},
Audiences: []string{"https://api.example.com"},
ClockSkew: 5 * time.Second, // tolerance for exp/nbf
RequireExpiry: true, // RFC 8725 §4.4 — strongly recommended
}))
mux.GET("/api/profile", func(w http.ResponseWriter, r *http.Request) {
claims, _ := middleware.GetJWTClaims(r.Context())
fmt.Fprintf(w, "user: %s\n", claims.Subject)
})// Validate tokens via RFC 7662 introspection (with caching):
mux.Use(middleware.OAuth2Introspect(middleware.OAuth2Options{
Endpoint: "https://idp.example.com/oauth/introspect",
ClientID: "my_service",
ClientSecret: os.Getenv("OAUTH2_SECRET"),
CacheTTL: 60 * time.Second, // cache active tokens
MaxCacheSize: 10000,
}))
mux.GET("/api/resource", func(w http.ResponseWriter, r *http.Request) {
introspect, _ := middleware.GetOAuth2Claims(r.Context())
fmt.Fprintf(w, "scope: %s\n", introspect.Scope)
})Three middleware components have backwards-compatible defaults that are
unsafe in production. The defaults exist so trivial test code keeps
working; production deployments MUST opt into the safer values listed
below. Each middleware emits a slog.Warn at construction time when it
detects the unsafe default — search your startup logs for those warnings.
Full discussion is in SECURITY.md.
| Middleware | Unsafe default | Safe production setting | Finding ID |
|---|---|---|---|
JWTAuth |
RequireExpiry: false accepts tokens with no exp claim — replayable forever |
RequireExpiry: true (per RFC 8725 §4.4) |
TM-2026-001 |
RealIP() |
Called with no CIDRs trusts every peer to spoof XFF / X-Real-IP | RealIP(&proxyCIDR) with the trusted reverse-proxy CIDR list |
TM-2026-044 |
OAuth2Introspect |
AllowInsecureEndpoint: true sends bearer tokens over plaintext |
Leave AllowInsecureEndpoint: false (default) — HTTPS endpoint only |
MSR-2026-0067 |
The examples/jwt, examples/oauth2, and examples/authn programs
demonstrate the hardened pattern (see also "Composite token-handling
stack" / CDX-S8-001 in SECURITY.md):
trusted := netip.MustParsePrefix("10.0.0.0/8")
// (1) Pre runs before dispatch — covers Handle and HandleFast routes.
mux.Pre(middleware.RealIP(&trusted))
// (2) Use composes inside the dispatch chain on stdlib routes.
mux.Use(
middleware.ThrottlePerIP(100, 5*time.Second, nil),
middleware.JWTAuth(middleware.JWTOptions{
Secret: secret,
Algorithms: []string{"HS256"},
RequireExpiry: true, // required in production
}),
)handler, params, found := mux.Lookup("GET", "/users/42")
if found {
fmt.Printf("found: %v params\n", len(params))
}routes := mux.Routes()
for _, route := range routes {
fmt.Printf("%-8s %s → %s\n", route.Method, route.Pattern, route.Handler)
}Walk visits every http.Handler route. FastHandler routes are skipped — use WalkFast to visit them:
err := mux.Walk(func(method, pattern string, handler http.Handler) error {
fmt.Printf("%s %s\n", method, pattern)
return nil
})err := mux.WalkFast(func(method, pattern string, handler muxmaster.FastHandler) error {
fmt.Printf("%s %s (FastHandler)\n", method, pattern)
return nil
})Medians from go test -bench=. -benchmem -count=5 -benchtime=3s. Two hardware platforms; full raw data under reports/.
| Route type | MuxMaster (default) | MuxMaster + PoolRequestBundle¹ |
httprouter | chi v5 |
|---|---|---|---|---|
| Static | 14 ns, 0 allocs | 14 ns, 0 allocs | 14.7 ns, 0 allocs | 114 ns, 2 allocs |
| 1 parameter | 57 ns, 1 alloc | 28 ns, 0 allocs | 33.0 ns, 1 alloc | 196 ns, 4 allocs |
| 2 parameters | 64 ns, 1 alloc | 36 ns, 0 allocs | 39.6 ns, 1 alloc | 226 ns, 4 allocs |
| 3 parameters | 70 ns, 1 alloc | 39 ns, 0 allocs | 45.1 ns, 1 alloc | 225 ns, 4 allocs |
| Catch-all | 58 ns, 1 alloc | 29 ns, 0 allocs | 27.3 ns, 1 alloc | 175 ns, 4 allocs |
| Parallel static | 1.86 ns, 0 allocs | 1.86 ns, 0 allocs | 2.35 ns, 0 allocs | 120 ns, 2 allocs |
| Parallel 1 param | 112 ns, 1 alloc | 10.2 ns, 0 allocs | 18.9 ns, 1 alloc | 188 ns, 4 allocs |
| Route type | MuxMaster (default) | MuxMaster + PoolRequestBundle¹ |
httprouter | chi v5 |
|---|---|---|---|---|
| Static | 25 ns, 0 allocs | 25 ns, 0 allocs | 33.8 ns, 0 allocs | 213.5 ns, 2 allocs |
| 1 parameter | 105 ns, 1 alloc | 45 ns, 0 allocs | 56.4 ns, 1 alloc | 354.1 ns, 4 allocs |
| 2 parameters | 119 ns, 1 alloc | 57 ns, 0 allocs | 66.5 ns, 1 alloc | 402.2 ns, 4 allocs |
| 3 parameters | 135 ns, 1 alloc | 59 ns, 0 allocs | 78.4 ns, 1 alloc | 410.2 ns, 4 allocs |
| Catch-all | 108 ns, 1 alloc | 44 ns, 0 allocs | 51.3 ns, 1 alloc | 330.2 ns, 4 allocs |
| Parallel static | 3.6 ns, 0 allocs | 3.6 ns, 0 allocs | 4.92 ns, 0 allocs | 128.2 ns, 2 allocs |
| Parallel 1 param | 100 ns, 1 alloc | 6.3 ns, 0 allocs | 22.2 ns, 1 alloc | 223.9 ns, 4 allocs |
¹ Mux.PoolRequestBundle = true recycles the per-request bundle via sync.Pool — zero allocations on param routes. Handlers must not retain *http.Request past return; see the Maximum Performance Guide.
Reproduce with:
go test -bench=. -benchmem ./...
Notes:
- MuxMaster allocates zero bytes for static routes and one tiered allocation (384–480 B) for parameterized routes in the default configuration. That single allocation fuses the copied
*http.Requestand its context — meaning the router, net/http, and your handler all share one GC object. - With
Mux.PoolRequestBundle = true, the parameterized-route allocation is recycled viasync.Pooland the count drops to zero as well. - httprouter's 1 alloc for parameterized routes is only a 64 B
Paramsslice; it passes parameters via a third argument outside thehttp.Handlerinterface, requiring a different handler signature. - MuxMaster is 100%
net/httpcompatible — it acceptshttp.Handlerdirectly, works with all existing middleware ecosystems, and requires no handler signature changes.
Extended documentation is in the docs/ directory:
| Guide | Description |
|---|---|
| Getting Started | Step-by-step guide for building your first application |
| Routing | Complete routing reference — syntax, methods, patterns |
| Middleware | Writing and composing middleware |
| Groups | Organizing routes with groups and sub-routers |
| Error Handling | Centralized error handling patterns |
| Configuration | All router options with defaults and examples |
| Response Helpers | JSON, XML, Text, Redirect, NoContent |
| Performance | How MuxMaster achieves zero allocations |
| Maximum Performance Guide | Configure PoolRequestBundle / PoolFastParams for zero-alloc dispatch — beats httprouter with 0 allocations |
| Migration Guide | Migrating from gorilla/mux, chi, and httprouter |
| Cookbook | Common patterns and production recipes |
Full API reference is available on pkg.go.dev.
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.
In brief:
- Fork the repository and create a feature branch.
- Run
go test -race ./...andgolangci-lint runbefore pushing. - Open a pull request against
main.
MIT — © 2026 Flavio CF Oliveira
