Files
httpmw/CLAUDE.md
Rene Nochebuena ad2a9e465e feat(httpmw): initial stable release v0.9.0
Standalone net/http middleware for panic recovery, CORS, request ID injection, and request logging.

What's included:
- Recover(): panic -> 500, captures debug.Stack, no logger required
- CORS(origins): OPTIONS 204 preflight, origin allowlist, package-wide method/header constants
- RequestID(generator): injects ID via logz.WithRequestID, sets X-Request-ID response header
- RequestLogger(logger): logs method/path/status/latency/request_id; Error for 5xx, Info otherwise
- Logger interface: Info, Error, With — duck-typed; satisfied by logz.Logger
- StatusRecorder: exported http.ResponseWriter wrapper that captures written status code
- Direct logz import for context helpers (documented exception to ADR-001)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 00:03:24 +00:00

4.6 KiB

httpmw

net/http middleware for transport-layer concerns: panic recovery, CORS, request ID injection, and structured request logging.

Purpose

httpmw provides standalone middleware functions that wrap http.Handler. Each function addresses one transport concern and is independent of the others. No authentication or identity logic lives here — see httpauth-firebase for that.

Tier & Dependencies

Tier: 3 (transport layer; depends on Tier 0 logz) Module: code.nochebuena.dev/go/httpmw Direct imports: code.nochebuena.dev/go/logz

The logz import is required for context helpers (logz.WithRequestID, logz.GetRequestID). The Logger injection point remains duck-typed. See ADR-001 for the full justification.

Key Design Decisions

  • Direct logz import for context helpers (ADR-001): logz.WithRequestID and logz.GetRequestID use an unexported key. Re-implementing the key in httpmw would break interoperability with logz.Logger.WithContext downstream. This is a documented exception to the global ADR-001 duck-type rule.
  • Request ID via logz context (ADR-002): RequestID middleware stores the generated ID with logz.WithRequestID so it is automatically picked up by any logz.Logger that calls .WithContext(ctx). The ID is also written to the X-Request-ID response header.
  • CORS returns 204 for OPTIONS (ADR-003): Preflight requests are short-circuited with 204 No Content after setting the CORS headers. Non-OPTIONS requests continue to the next handler.
  • Recover requires no logger injection: It writes 500 and captures the stack trace (via debug.Stack) but does not log. The stack is available for future logger injection if needed. This keeps Recover usable with zero configuration.
  • No middleware is installed by default: The package exports functions, not a pre-configured chain. The application chooses which middleware to use and in what order.

Patterns

Recommended middleware order (outermost first):

// 1. Recover — must be outermost to catch panics from all inner middleware
// 2. RequestID — generates ID early so it is available to logger
// 3. RequestLogger — reads ID from context; logs after inner handlers complete
// 4. CORS — sets headers before business logic runs

mux.Use(httpmw.Recover())
mux.Use(httpmw.RequestID(uuid.NewString))
mux.Use(httpmw.RequestLogger(logger))
mux.Use(httpmw.CORS([]string{"https://example.com"}))

Logger interface (duck-typed; satisfied by logz.Logger):

type Logger interface {
    Info(msg string, args ...any)
    Error(msg string, err error, args ...any)
    With(args ...any) Logger
}

StatusRecorder is exported for use by custom logging middleware that needs to inspect the written status code after the inner handler returns:

rec := &httpmw.StatusRecorder{ResponseWriter: w, Status: http.StatusOK}
next.ServeHTTP(rec, r)
fmt.Println(rec.Status)

What to Avoid

  • Do not put authentication, identity checks, or RBAC logic in this package. Those belong in httpauth-firebase and operate on rbac.Identity.
  • Do not define your own context key for the request ID. Use logz.WithRequestID and logz.GetRequestID so the ID is visible to logz-enriched log records.
  • Do not rely on Recover to log panics — it currently only writes a 500 response. If panic logging is required, wrap Recover with a custom middleware that also logs, or wait for logger injection to be added.
  • Do not configure per-route CORS using this middleware. The allowed methods and headers constants are package-wide. Use your router's built-in CORS support if per-route configuration is needed.
  • Do not change the order so that RequestID comes after RequestLogger. The logger reads the request ID from context; if RequestID has not run yet, the ID will be empty in log records.

Testing Notes

  • compliance_test.go verifies at compile time that any struct implementing Info, Error, and With satisfies httpmw.Logger, and that StatusRecorder satisfies http.ResponseWriter.
  • httpmw_test.go uses httptest.NewRecorder() and httptest.NewRequest() for all tests — no real network connections.
  • Recover can be tested by constructing a handler that panics and asserting the response is 500.
  • RequestLogger depends on logz.GetRequestID; tests should run the handler through a RequestID middleware first, or call logz.WithRequestID on the request context manually.
  • CORS with []string{"*"} matches any origin — useful for table-driven tests covering allowed vs. rejected origins.