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).
107 lines
4.6 KiB
Markdown
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.
|