Skip to content

willunylabs/wand

Repository files navigation

Wand 🪄

CI Go Report Card Go Reference License: MIT

High-Performance, Zero-Allocation HTTP Router for Go

Go version: 1.24.13+ (patched standard library).

wand is a minimalist, infrastructure-grade HTTP router and toolkit designed for services where latency and memory efficiency are critical. It features a lock-free design, zero-allocation routing paths, and effective DoS protection.

Wand /wɒnd/ - A symbol of magic and control. Elegantly directing traffic with precision and speed.

Philosophy

Wand is a router, not a framework. We believe:

  • 🎯 Do One Thing Well: Route HTTP requests efficiently, nothing more.
  • 🧩 Compose, Don't Replace: Integrate with Go's ecosystem instead of reinventing it.
  • Performance Matters: Zero allocations on the hot path, but not at the cost of usability.
  • 📖 Explicit Over Magic: No reflection, no code generation, no surprises.

If you need a batteries-included framework, consider Gin or Echo. If you want control, you're in the right place.

Features

  • Zero Allocation: Optimized hot paths (static, dynamic, and wildcard routes) generate 0 bytes of garbage per request.
  • Zero-Alloc Param Extraction: Params are captured via pooled slices and index offsets—no maps or context allocations.
  • High Performance:
    • Static Routes: ~41ns
    • Dynamic Routes: ~105ns
  • DoS Protection: Built-in limits for MaxPathLength (4096) and MaxDepth (50) to prevent algorithmic complexity attacks.
  • Frozen Mode: Innovative FrozenRouter flattens static path segments for extreme read-heavy performance.
  • Lock-Free Logger: Specific high-throughput RingBuffer logger implementation.
  • Minimalist Middleware: Includes essential middlewares (Logger, Recovery, RequestID, AccessLog, Timeout, BodySizeLimit, CORS, Static, Gzip, Metrics).
  • Pre-composed Middleware: Router.Use and Group build middleware chains at registration time (no per-request wrapping).
  • Custom 404/405: Optional NotFound and MethodNotAllowed handlers.
  • Strict Slash (Default: on): Redirects /path <-> /path/ to the registered canonical path.
  • UseRawPath (Optional): Match on encoded paths and return encoded params. When RawPath is valid, matching skips decoded-path cleaning/redirects; invalid RawPath falls back to Path (see Path Semantics & Security).

Installation

go get github.com/willunylabs/wand

First 5 Minutes

Start with the smallest setup in this order: NewRouter -> Use -> Group/Register -> Start.

package main

import (
	"log"
	"net/http"

	"github.com/willunylabs/wand/middleware"
	"github.com/willunylabs/wand/router"
)

func main() {
	r := router.NewRouter()

	// 1) Register global middlewares first.
	if err := r.Use(
		middleware.Recovery,
		middleware.RequestID,
	); err != nil {
		panic(err)
	}

	// 2) Create groups and register routes.
	api := r.Group("/api")
	_ = api.GET("/health", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("OK"))
	})
	_ = api.GET("/users/:id", func(w http.ResponseWriter, _ *http.Request) {
		id, _ := router.Param(w, "id")
		_, _ = w.Write([]byte(id))
	})

	// 3) Start the HTTP server.
	log.Fatal(http.ListenAndServe(":8080", r))
}

Do not do these:

  • Call Use(...) after any route has been registered.
  • Ignore route registration errors from GET/POST/....
  • Enable UseRawPath without a clear proxy normalization policy.

For large route sets, you can reduce wiring noise with startup-only helpers:

  • MustGET/MustPOST/... when you want fail-fast registration (panic on invalid route).
  • Routes(...router.Route) for table-style batch registration.

Quick Start (Production-Oriented)

package main

import (
	"context"
	"net/http"
	"time"

	"github.com/willunylabs/wand/logger"
	"github.com/willunylabs/wand/middleware"
	"github.com/willunylabs/wand/router"
	"github.com/willunylabs/wand/server"
)

type noopMetricRecorder struct{}

func (noopMetricRecorder) ObserveHTTP(middleware.HTTPMetric) {}

func main() {
	// 1) Create a new router.
	r := router.NewRouter()

	// 2) Setup high-performance logger.
	rb, _ := logger.NewRingBuffer(1024)
	go rb.Consume(func(events []logger.LogEvent) {
		// batch process logs
	})

	// 3) Compose middleware (must be called before registering routes).
	if err := r.Use(
		middleware.Recovery,
		middleware.RequestID,
		func(next http.Handler) http.Handler { return middleware.AccessLog(rb, next) },
		func(next http.Handler) http.Handler { return middleware.BodySizeLimit(1<<20, next) },
		middleware.Gzip,
		func(next http.Handler) http.Handler { return middleware.Timeout(5*time.Second, next) },
		func(next http.Handler) http.Handler { return middleware.Metrics(noopMetricRecorder{}, next) },
	); err != nil {
		panic(err)
	}

	// 4) Create groups and register routes.
	api := r.Group("/api")

	_ = api.GET("/health", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	})

	_ = api.GET("/users/:id", func(w http.ResponseWriter, req *http.Request) {
		id, _ := router.Param(w, "id")
		w.Write([]byte("User ID: " + id))
	})

	// 5) Start graceful server.
	srv := &http.Server{
		Addr:    ":8080",
		Handler: r,
	}

	ctx, cancel := server.SignalContext(context.Background())
	defer cancel()
	_ = server.Run(ctx, srv, 5*time.Second)
}

Golden-path sample app:

go run ./cmd/example-api

Logger (Text / JSON)

Use these in your startup setup phase before route registration.

// Text (default)
_ = r.Use(middleware.Logger)

// JSON
_ = r.Use(middleware.LoggerWith(middleware.LoggerOptions{
	JSON: true,
}))

Third-Party Middleware

Wand uses standard http.Handler middleware signatures, so you can plug in any third-party middleware (JWT, OTEL, Prometheus, etc.) directly. Apply them during startup before registering routes:

// Example: third-party JWT middleware
// jwtmw := somejwt.New(...)
// _ = r.Use(jwtmw)

Common Setup Mistakes

  1. Calling Use(...) after route registration.
  • Wrong order:
_ = r.GET("/health", h)
_ = r.Use(middleware.Recovery) // returns error
  • Correct order:
_ = r.Use(middleware.Recovery)
_ = r.GET("/health", h)
  1. Reading params from req.Context() directly.
  • Use router.Param(w, "id") (or w.(interface{ Param(string) (string, bool) })) in handlers.
  1. Mixing encoded and decoded path normalization accidentally.
  • Only enable UseRawPath when your reverse proxy and origin agree on one normalization layer.

Components

  • router: Path-Segment Trie based, zero-alloc HTTP router.
  • middleware: Essential HTTP middlewares.
  • httpx: Optional JSON/error helpers for standardized handlers.
  • logger: Lock-free ring buffer logger for high-throughput scenarios.
  • server: Helpers for graceful server shutdown.
  • auth: Minimal identity/authenticator interfaces.

Guides

  • docs/getting_started.md — first-run setup, order-of-operations, and troubleshooting.
  • docs/spec_router_semantics.md — v1 routing behavior contract and compatibility scope.
  • docs/httpx_error_and_json.md — optional handler helpers for JSON decode/write and error envelopes.
  • docs/production_blueprint.md — small-team production blueprint (middleware order, timeout budget, trusted proxy, shutdown).
  • docs/metrics_contract.md — official HTTP metrics fields and behavior contract.
  • docs/http_status_and_error_contract.md — status and error-envelope conventions.
  • docs/server.md — production/development server templates.
  • docs/security.md — deployment hardening and safety notes.
  • docs/production_checklist.md — production security checklist.
  • docs/integrations.md — compression, rate limiting, trusted proxy parsing.
  • docs/observability.md — Prometheus/OTel/pprof integration.
  • docs/versioning_and_compat.md — v1 compatibility policy and API gate behavior.
  • docs/upgrade_and_rollback.md — staged upgrade and rollback runbook.
  • docs/auth.md — auth interfaces with JWT/session examples.
  • docs/migration_gin_echo.md — migration map from Gin/Echo with common pitfalls.

Server Best Practices

See docs/server.md for production/development http.Server templates and timeout guidance.

Recommended middleware order for consistent panic/timeout logging:

  1. Recovery
  2. RequestID
  3. AccessLog
  4. BodySizeLimit
  5. Timeout
  6. Metrics

Path Semantics & Security

  • Default behavior: routing matches against URL.Path (decoded). Non-canonical paths are normalized with cleanPath and redirected to the canonical path.
  • UseRawPath: when enabled and URL.RawPath is valid (RawPath == EscapedPath()), routing matches the encoded path and returns encoded params. In this mode, decoded-path cleaning/redirects are skipped.
  • Fallback: if RawPath is invalid or inconsistent, routing falls back to URL.Path (decoded) and canonicalization/redirects apply.
  • StrictSlash + RawPath: when UseRawPath is active, trailing-slash redirects preserve the encoded form to avoid changing path semantics.
  • Security note: ensure your reverse proxy and app agree on a single normalization layer. If an upstream proxy decodes %2F to / while the router matches encoded paths, you can get route bypass or mismatch. Avoid double decoding and document the chosen layer for your deployment.
  • pprof note: debug endpoints require an explicit allow policy; see docs/observability.md for the safe pattern.

CORS Notes

  • AllowedOrigins: ["*"] with AllowCredentials: true is rejected for safety. Use an explicit allowlist or AllowOriginFunc.

Logger Notes

  • RingBuffer.Consume will re-panic if the consumer handler panics, unless PanicHandler is set. Set PanicHandler to record/alert on failures, but avoid silently dropping log batches.

Non-Goals

Wand will not include:

  • ❌ Certificate management (use autocert, Caddy, or your cloud provider)
  • ❌ A proprietary metrics system (use Prometheus/OTEL)
  • ❌ A full web framework (no ORM, no "App" struct, no magic)
  • ❌ Complex binding/validation (use go-playground/validator)

Performance

Router Microbench (2026-02-20)

Run on Apple M4 Pro (local run):

Benchmark ns/op B/op allocs/op
BenchmarkRouter_Static 43.32 0 0
BenchmarkRouter_Dynamic 107.4 0 0
BenchmarkRouter_Wildcard 85.86 0 0
BenchmarkFrozen_Static 39.83 0 0
BenchmarkFrozen_Dynamic 112.9 0 0
BenchmarkFrozen_Wildcard 90.13 0 0
BenchmarkRouter_MethodNotAllowed 56.28 0 0
BenchmarkFrozen_MethodNotAllowed 51.45 0 0
BenchmarkRouter_Options 55.27 0 0
BenchmarkFrozen_Options 51.15 0 0

GitHub API Benchmark

Comparative results for the GitHub API routing benchmark. Run on Apple M4 Pro (Go 1.23).

Benchmark name (1) (2) (3) (4)
BenchmarkGin_GithubAll 143499 8386 ns/op 0 B/op 0 allocs/op
BenchmarkHttpRouter_GithubAll 127113 9165 ns/op 13792 B/op 167 allocs/op
BenchmarkEcho_GithubAll 118155 10437 ns/op 0 B/op 0 allocs/op
BenchmarkWand_GithubAll 46750 24595 ns/op 0 B/op 0 allocs/op
BenchmarkChi_GithubAll 24181 49981 ns/op 130904 B/op 740 allocs/op
BenchmarkGorillaMux_GithubAll 1062 1138474 ns/op 225922 B/op 1588 allocs/op

(1): Total Repetitions (higher is better) (2): Latency (ns/op) (lower is better) (3): Heap Memory (B/op) (lower is better) (4): Allocations (allocs/op) (lower is better)

Quality Gates

  • CI: build, go vet, tests, and race (.github/workflows/ci.yml).
  • Coverage gate: scripts/coverage-check.sh (default thresholds: total >= 80%, router >= 80%, middleware >= 78%, logger >= 90%, auth >= 80%).
  • API compatibility gate: scripts/api-compat.sh (checks against latest release tag).
  • Lint: golangci-lint (.github/workflows/linter.yml).
  • Security: govulncheck (.github/workflows/vuln.yml).
  • Static analysis: gosec (.github/workflows/security.yml).
  • Supply chain: SBOM (.github/workflows/sbom.yml) + Dependabot (.github/dependabot.yml).
  • Scheduled fuzz/bench: .github/workflows/fuzz.yml, .github/workflows/bench.yml.
  • Nightly stability: .github/workflows/stability.yml (race + soak + benchmark snapshots).
  • Benchmark regression gate: benchmarks/baseline.txt + BENCH_MAX_REGRESSION_PCT (see benchmarks/README.md).

Security Guide

See docs/security.md for deployment hardening, proxy alignment, and CORS/logging safety notes.

License

MIT