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/
47 lines
2.4 KiB
Markdown
47 lines
2.4 KiB
Markdown
# ADR-003: CORS Preflight Returns 204 No Content
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-03-18
|
|
|
|
## Context
|
|
|
|
CORS preflight requests use the HTTP `OPTIONS` method. Browsers send them before
|
|
cross-origin requests that carry non-simple headers or methods, and they expect a
|
|
response with `Access-Control-Allow-*` headers. The response status code of a
|
|
preflight is not consumed by the browser's fetch algorithm — only the headers
|
|
matter — but the choice of status code affects certain clients and proxies.
|
|
|
|
Common practice uses either 200 OK or 204 No Content for OPTIONS responses.
|
|
RFC 9110 specifies that 204 indicates "the server has successfully fulfilled the
|
|
request and there is no additional content to send". This is semantically more
|
|
accurate for a preflight: there is no payload, only permission headers.
|
|
|
|
## Decision
|
|
|
|
The `CORS` middleware checks `r.Method == http.MethodOptions` after writing the
|
|
CORS headers. If the method is OPTIONS, it calls `w.WriteHeader(http.StatusNoContent)`
|
|
and returns immediately without calling `next.ServeHTTP`. The downstream handler is
|
|
never reached for preflight requests.
|
|
|
|
Non-OPTIONS requests that pass the origin check continue to `next` with the
|
|
`Access-Control-Allow-Origin` and related headers already set on the response.
|
|
|
|
If the request origin is not in the allowed list, CORS headers are not set and the
|
|
request continues to `next` unchanged — the browser will block the response at its
|
|
end, but the server does not return an explicit rejection.
|
|
|
|
## Consequences
|
|
|
|
- 204 is semantically cleaner than 200 for no-body responses, and avoids some
|
|
proxy caches treating the OPTIONS response as cacheable content.
|
|
- Some legacy clients or API testing tools expect 200 for OPTIONS. If this is a
|
|
concern, the response status can be changed at the application level by wrapping
|
|
the `CORS` middleware, but 204 is the default and documented contract.
|
|
- The `allowedMethods` constant (`GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS`)
|
|
and `allowedHeaders` constant (`Content-Type, Authorization, X-Request-ID`) are
|
|
package-level constants, not configurable per route. Applications with fine-grained
|
|
per-route CORS requirements should configure their router's CORS support instead.
|
|
- For local development, passing `[]string{"*"}` as `origins` is supported: any
|
|
origin header is treated as allowed, and CORS headers are set with the actual
|
|
origin value (not `*`) to support credentials if needed.
|