CORS preflight and OPTIONS
Ever saw an OPTIONS request before your POST? That is a CORS preflight. Here is when it happens and how to respond.
When does it fire?
This concerns cross-origin CORS requests such as fetch. A same-origin JSON POST does not require a CORS preflight. A matching entry in the dedicated preflight cache can avoid OPTIONS.
Typical triggers
- Methods other than GET/HEAD/POST (PUT/PATCH/DELETE, etc.).
- A Content-Type outside the CORS safelist, such as application/json. Safelisted media types are application/x-www-form-urlencoded, multipart/form-data, and text/plain, with additional value-length and character restrictions.
- Script-specified headers outside the safelist, such as Authorization or X-… headers. Fetch defines other triggers too, including streaming request bodies.
Headers the browser sends
Preflight asks about the actual request. It does not send cookies or the actual Authorization value, but announces the intended method and relevant header names. Authentication and authorization for the operation belong on the actual request.
Key headers
| Header | Meaning |
|---|---|
| Origin | Usually the requesting scheme, host, and port. Default ports are omitted; some contexts serialize the origin as null. |
| Access-Control-Request-Method | Method intended for the actual request. |
| Access-Control-Request-Headers | Names requiring permission checks, lowercased and comma-separated, rather than their values. |
How to respond
Success needs a 2xx response and appropriate CORS headers. 204 has no body; a 200 body is unused by the preflight check. The ordinary Allow header does not replace Access-Control-Allow-Methods.
Allow headers
| Header | Role |
|---|---|
| Access-Control-Allow-Origin | A single allowed origin or *. The wildcard is invalid when the actual credentials mode is include. Validate origins against an allowlist instead of reflecting any input. |
| Access-Control-Allow-Methods | Allowed methods, compared case-sensitively. Explicitly allow non-safelisted methods such as PUT. |
| Access-Control-Allow-Headers | Allowed request-header names, compared case-insensitively. Fetch excludes Authorization from *; list it explicitly rather than relying on implementation differences. |
| Access-Control-Max-Age | Lifetime in the dedicated preflight cache, not the HTTP cache. The default is 5 seconds; browsers can cap it or discard entries earlier. |
| Access-Control-Allow-Credentials | Lowercase true is required when the actual credentials mode is include. This is separate from allowing a script-specified Authorization header via Allow-Headers. |
Status codes
- 204 No Content is simplest; 200 is also fine, but the body is unused.
- A disallowed preflight can return 403. Passing CORS does not remove the need for authentication and authorization on the actual request.
A JSON request example
Assume a POST from https://app.example to https://api.example/items with credentials: "omit", a JSON body, and an explicit Authorization header. These are relevant preflight headers; the authorization value is not sent.
Preflight request
OPTIONS /items HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type
Permission response
The request uses lowercase names and the response mixed case; they match. With credentials mode omit, Allow-Credentials is unnecessary here. Authenticate the subsequent POST and include Allow-Origin on its response too.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Common pitfalls
Inspect the preflight result separately from the actual response to narrow down failures.
Checklist
- Access-Control-Allow-Headers matches names case-insensitively. Check that all required names are allowed.
- With credentials: "include", use a specific Allow-Origin and Allow-Credentials: true. * in Allow-Methods/Allow-Headers is not a wildcard in this mode. Cookie SameSite and browser restrictions still apply.
- Requiring a login cookie on preflight causes failure. Return the permission policy for OPTIONS and authenticate the actual operation. Origin is not authentication: clients such as curl can set it freely.
- The actual response also needs the appropriate Allow-Origin and, when required, Allow-Credentials. If cached actual responses vary by Origin, consider Vary: Origin.
Takeaway
Some requests do not need preflight, so CORS is not a substitute for CSRF protection or server authentication. See method properties and ordinary HTTP caching.
Specifications
Fetch: CORS protocol / Fetch: CORS-preflight fetch / Fetch: CORS-preflight cache / RFC 9110 §9.3.7