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/
2.4 KiB
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
CORSmiddleware, but 204 is the default and documented contract. - The
allowedMethodsconstant (GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS) andallowedHeadersconstant (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{"*"}asoriginsis supported: any origin header is treated as allowed, and CORS headers are set with the actual origin value (not*) to support credentials if needed.