Files
httpmw/docs/adr/ADR-003-cors-204-preflight.md

47 lines
2.4 KiB
Markdown
Raw Permalink Normal View History

# 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.