Files
httpmw/CLAUDE.md
Rene Nochebuena 32831d5a06 docs(httpmw): correct tier from 3 to 2 and fix logz tier ref
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).
2026-03-19 06:55:59 -06:00

107 lines
4.6 KiB
Markdown

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