Files
httpmw/CLAUDE.md

107 lines
4.6 KiB
Markdown
Raw Normal View History

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