httpmw only depends on logz (Tier 1), placing it at Tier 2. The previous docs incorrectly stated both the module tier (3) and the logz tier (0).
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: 2 (transport layer; depends on Tier 1 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.WithRequestIDandlogz.GetRequestIDuse an unexported key. Re-implementing the key inhttpmwwould break interoperability withlogz.Logger.WithContextdownstream. This is a documented exception to the global ADR-001 duck-type rule. - Request ID via logz context (ADR-002):
RequestIDmiddleware stores the generated ID withlogz.WithRequestIDso it is automatically picked up by anylogz.Loggerthat calls.WithContext(ctx). The ID is also written to theX-Request-IDresponse 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.
Recoverrequires no logger injection: It writes 500 and captures the stack trace (viadebug.Stack) but does not log. The stack is available for future logger injection if needed. This keepsRecoverusable 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-firebaseand operate onrbac.Identity. - Do not define your own context key for the request ID. Use
logz.WithRequestIDandlogz.GetRequestIDso the ID is visible to logz-enriched log records. - Do not rely on
Recoverto log panics — it currently only writes a 500 response. If panic logging is required, wrapRecoverwith 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
RequestIDcomes afterRequestLogger. The logger reads the request ID from context; ifRequestIDhas not run yet, the ID will be empty in log records.
Testing Notes
compliance_test.goverifies at compile time that any struct implementingInfo,Error, andWithsatisfieshttpmw.Logger, and thatStatusRecordersatisfieshttp.ResponseWriter.httpmw_test.gouseshttptest.NewRecorder()andhttptest.NewRequest()for all tests — no real network connections.Recovercan be tested by constructing a handler that panics and asserting the response is 500.RequestLoggerdepends onlogz.GetRequestID; tests should run the handler through aRequestIDmiddleware first, or calllogz.WithRequestIDon the request context manually.CORSwith[]string{"*"}matches any origin — useful for table-driven tests covering allowed vs. rejected origins.