# 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):** ```go // 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`): ```go 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: ```go 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.