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:
2026-03-19 00:03:24 +00:00
commit ad2a9e465e
17 changed files with 739 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# ADR-002: Request ID Injected via logz Context Helpers
**Status:** Accepted
**Date:** 2026-03-18
## Context
Each HTTP request should carry a unique identifier that appears in log records,
error responses, and the `X-Request-ID` response header, so that a single request
can be traced across log lines.
There are two sub-problems:
1. **Generation and storage**: the middleware must generate an ID and make it
available to downstream code via the request context.
2. **Retrieval for logging**: `RequestLogger` must be able to read the ID from the
context to include it in log records.
The naive approach — store the ID under a locally-defined context key — breaks
interoperability with `logz.Logger.WithContext`, which enriches log records with
values stored under `logz`'s own unexported key. If a different key is used, logz
cannot find the ID, and it does not appear automatically in structured log output.
## Decision
The `RequestID` middleware calls `logz.WithRequestID(ctx, id)` to store the
generated ID in context. `RequestLogger` calls `logz.GetRequestID(r.Context())` to
retrieve it.
Both functions use the same unexported key inside the `logz` package, guaranteeing
that the value stored by `RequestID` is the same value retrieved by `RequestLogger`
and, more importantly, by any `logz.Logger` downstream that calls `WithContext`.
The ID is also written to the `X-Request-ID` response header at the middleware
level, so clients can correlate responses to requests without parsing log files.
## Consequences
- Any `logz.Logger` in the request chain that calls `.WithContext(ctx)` automatically
inherits the request ID as a structured log field — no manual plumbing required.
- The generator function is injected by the caller (`RequestID(generator func() string)`),
keeping the ID format flexible (UUID, ULID, or any string).
- If the same request ID is sent in the `X-Request-ID` request header, it is ignored
by `RequestID` — the middleware always generates a fresh ID. Preserving inbound
IDs is a separate concern that should be handled explicitly if required.
- This design requires `httpmw` to import `logz` directly (see ADR-001 for the
justification of that exception).
- Global ADR-003 (context helpers live with data owners) is directly applied here:
`logz` owns the request ID context key because the request ID is a logging
concern, not an auth or business concern.