The CORS preflight flow
Before a cross-origin request that is not “simple”, the browser sends an automatic OPTIONS preflight asking the server which method and headers are allowed. This reference pairs every Access-Control-Request-* header with the Access-Control-Allow-* response the server must return, and an analyzer tells you whether a given request preflights.
Why preflight exists — the underlying design
CORS preflight is not a performance feature. It is a security mechanism built around the Same-Origin Policy. Before CORS existed, browsers prevented cross-origin reads entirely. As the web evolved and APIs proliferated, a controlled way to opt in to cross-origin access was needed — but without enabling a malicious page to silently trigger state-changing requests (like a DELETE or a PUT) against a user’s authenticated session on another origin.
The preflight acts as an explicit consent check: the browser asks the server “are you willing to accept a cross-origin request using this method and these headers from this origin?” before sending the actual request. Servers that have not been configured to respond correctly to OPTIONS simply do not respond, and the real request never fires.
The two paths: simple vs. preflighted
A request takes the simple path (no preflight) when all three conditions hold:
- The method is
GET,HEAD, orPOST. - Only CORS-safelisted request headers are used:
Accept,Accept-Language,Content-Language,Content-Type(with restrictions),Range. - Any
Content-Typeis one of:application/x-www-form-urlencoded,multipart/form-data, ortext/plain.
Anything outside these constraints — including application/json as the Content-Type, any custom header like Authorization, or a method like PUT, DELETE, or PATCH — triggers a preflight.
How it works
The browser preflights when the request uses a non-safelisted method or header. The preflight is an OPTIONS carrying the intended method and headers:
OPTIONS /api/data HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
The server must answer with matching allow headers:
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600
A request that uses only GET/HEAD/POST, safelisted headers and a safelisted Content-Type is “simple” and skips the preflight entirely.
The credentials exception — the most common misconfiguration
When a request sends credentials (cookies or HTTP authentication), Access-Control-Allow-Origin: * is not permitted. The browser will reject the response even if the server sends it. The server must echo the exact Origin value and also include Access-Control-Allow-Credentials: true.
This trips up developers who set a wildcard origin during development (where credentials may not be involved) and then add authentication later. The fix requires two changes simultaneously: replace * with the specific origin, and add the Allow-Credentials header.
The complete header pairing reference
| Browser sends | Server must return |
|---|---|
Access-Control-Request-Method: DELETE | Access-Control-Allow-Methods: GET, POST, DELETE |
Access-Control-Request-Headers: authorization | Access-Control-Allow-Headers: authorization |
Origin: https://app.example | Access-Control-Allow-Origin: https://app.example |
| (credentials flag set) | Access-Control-Allow-Credentials: true (and specific origin, not *) |
| (any preflight) | Access-Control-Max-Age: 600 (optional, caches result) |
Tips and notes
- With credentials, never use
*— echo the exactOriginand setAccess-Control-Allow-Credentials: true. Access-Control-Max-Agecaches the preflight result so the browser does not send an OPTIONS on every API call. Chromium caps the value at 7200 seconds; Firefox allows up to 86400 seconds.- List every custom request header in
Access-Control-Allow-Headers— a single missing header causes the preflight to fail silently from the developer’s perspective but with a clear error in the browser console. Access-Control-Expose-Headersis separate: it controls which response headers JavaScript can read. By default only a small set of safelisted response headers are accessible; add any custom headers here if your client code needs them.- Preflight failures appear in the browser network panel as a failed OPTIONS request, not as a failure on the actual intended request — check the OPTIONS response first when debugging.