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/
This commit is contained in:
106
CLAUDE.md
Normal file
106
CLAUDE.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user