Skip to content

FlavioCFOliveira/MuxMaster

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

171 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MuxMaster

MuxMaster

CI Go Reference Go Report Card Go Version Latest Release License: MIT

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.

Why MuxMaster?

  • 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 = true recycles the per-request bundle via sync.Pool, dropping 1-param routes to 45 ns / 0 B / 0 allocs (20 % faster than httprouter); see Maximum Performance Guide
  • 100% net/http compatible — drop in anywhere http.Handler is 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 handlersHandlerFuncE enables 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 introspectionLookup, Routes, Walk, and WalkFast for programmatic route inspection

Contents


Installation

go get github.com/FlavioCFOliveira/MuxMaster

Requires Go 1.26 or later.


Quick Start

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))
}

Route Syntax

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.

Registering routes

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))

Path Parameters

Reading a single parameter

mux.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
    id := muxmaster.PathParam(r, "id")
    fmt.Fprintf(w, "user: %s\n", id)
})

Reading all parameters

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)
})

Typed helpers

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

Catch-all parameters

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)
})

Regex-constrained parameters

// 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)
})

Reading parameters in middleware

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)
    })
}

The Param type

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)
    }
})

Route pattern

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

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.

Global middleware

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 Recoverer

Pre-routing middleware

Pre-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)

Per-route middleware with With

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)

Writing custom middleware

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)

Fast Routes

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) }()
}

Registering fast routes

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.

Fast middleware

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.

Fast routes in groups

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)

Performance and trade-offs

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 FastMiddleware only
  • Params are not in the request context — they are passed as a direct argument
  • Lookup() returns nil for FastHandler routes — use WalkFast to enumerate them
  • No convenience short-hand methods on Group (use group.HandleFast("GET", ...))

Maximum Performance Mode (Zero Allocations)

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

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).

Basic group

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/users

Nested groups

api := 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/:id

Inline groups with Route

Route 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)
    })
})

Scoped middleware with With

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)

Register multiple methods on a group

Match registers the same handler for a set of HTTP methods:

api := mux.Group("/api/v1")
api.Match([]string{"GET", "HEAD"}, "/status", statusHandler)

Mounting Sub-Routers

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 listItemsV2

Mounted handlers receive a request with the prefix stripped from r.URL.Path, so the sub-router sees /items, not /v2/items.


Static Files

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.


Error Handling

Error-returning handlers

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.

The HTTPError interface

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"))

Custom error handler

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"})
}

Custom 404 and 405 handlers

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",
    })
})

Panic recovery

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)
}

Custom OPTIONS handler

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.


Response Helpers

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)
})

Router Options

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 code

Resetting configuration

Configuration 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 snapshot

Warning: do not call Rebuild() while the server is actively serving requests.


Included Middleware

The middleware sub-package provides 17 production-ready middleware components. Import it separately:

import "github.com/FlavioCFOliveira/MuxMaster/middleware"

Overview

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

Usage examples

// 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"))

Authentication Middleware

Three authentication middleware components cover the most common scenarios.

API Key Authentication

// 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)
})

JWT Bearer Token Authentication

// 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)
})

OAuth2 Token Introspection

// 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)
})

Security defaults

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
    }),
)

Route Introspection

Check whether a route is registered

handler, params, found := mux.Lookup("GET", "/users/42")
if found {
    fmt.Printf("found: %v params\n", len(params))
}

List all registered routes

routes := mux.Routes()
for _, route := range routes {
    fmt.Printf("%-8s %s  →  %s\n", route.Method, route.Pattern, route.Handler)
}

Iterate routes with a callback

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
})

Iterate fast routes with a callback

err := mux.WalkFast(func(method, pattern string, handler muxmaster.FastHandler) error {
    fmt.Printf("%s %s (FastHandler)\n", method, pattern)
    return nil
})

Benchmarks

Medians from go test -bench=. -benchmem -count=5 -benchtime=3s. Two hardware platforms; full raw data under reports/.

Apple M4 — 10 cores, 32 GB, Go 1.26.2, macOS arm64 (2026-05-12)

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

AMD Ryzen 9 5900HX — 16 cores, Go 1.26.2, Linux 6.8 (2026-05-08)

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.Poolzero 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.Request and 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 via sync.Pool and the count drops to zero as well.
  • httprouter's 1 alloc for parameterized routes is only a 64 B Params slice; it passes parameters via a third argument outside the http.Handler interface, requiring a different handler signature.
  • MuxMaster is 100% net/http compatible — it accepts http.Handler directly, works with all existing middleware ecosystems, and requires no handler signature changes.

Documentation

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.


Contributing

Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.

In brief:

  1. Fork the repository and create a feature branch.
  2. Run go test -race ./... and golangci-lint run before pushing.
  3. Open a pull request against main.

License

MIT — © 2026 Flavio CF Oliveira

About

Pure go http router focused on performance maximization

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages