Glossary
Preflight request
A preflight is an `OPTIONS` request a browser sends before certain cross-origin requests, asking the server whether the real request is permitted. It exists because some requests can change state, and the web predates CORS: a server written before cross-origin requests existed must not be exposed to new ones without being asked. The cost is an extra round trip, which `Access-Control-Max-Age` exists to amortise.
What triggers one
Any method other than `GET`, `HEAD` or `POST`. `PUT`, `PATCH` and `DELETE` always preflight.
A `Content-Type` other than `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`. This is why almost every JSON API call preflights: `application/json` is not on the list.
Any request header beyond a short safe set — an `Authorization` header, a custom `X-` header, an API key header.
Requests that meet none of these conditions are called simple requests and are sent directly, because a form could have produced the same request before CORS existed.
A request whose body is a `ReadableStream`, or which registers an upload progress listener, also preflights regardless of its method and headers.
What the exchange looks like
The browser sends `OPTIONS` with `Access-Control-Request-Method` and `Access-Control-Request-Headers` describing what it intends to do.
The server answers with `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` stating what is permitted.
If the answer covers the intended request, the browser sends it. If not, the request is never sent and the page receives a CORS error.
The preflight itself carries no cookies unless credentials are requested, and its response must not require authentication — a server demanding a session for `OPTIONS` breaks every cross-origin client.
How to stop paying for it
`Access-Control-Max-Age` caches the preflight result for that origin, method and header combination. Browsers cap it — Chrome at two hours, Firefox at twenty-four — so a very large value is silently reduced.
Removing the trigger is better where possible: a request using only safe headers and a simple content type does not preflight at all.
Placing the API on the same origin as the page removes CORS from the picture entirely, which a reverse proxy can arrange without moving anything.
Batching requests reduces the number of preflights proportionally, which matters most on connections where the round trip is expensive.
How to diagnose a failure
In the network panel, look for the `OPTIONS` request before the failing one. Its response headers are the answer.
A missing `OPTIONS` handler is the most common cause: the server returns 404 or 405, and the real request is never attempted.
A method or header requested but not listed in the response is the second: the error names which, in the console.
`curl -X OPTIONS -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: PUT' -I https://api.example.com/resource` reproduces the exchange outside the browser.
Check `Vary: Origin` on the preflight response as well as on the real one. A cached preflight answer carrying the wrong origin is the same class of fault and is harder to reproduce.
Frequently asked questions
- Why does my JSON request send an OPTIONS first?
- Because application/json is not among the content types that avoid a preflight. Any request using it triggers one.
- Can I disable preflight requests?
- Not directly. You can avoid triggering them by using a simple content type and headers, or cache the result with Access-Control-Max-Age.
- Does the preflight include cookies?
- No, unless credentials are requested. Its response must also not require authentication, or every cross-origin client fails.
Sources
Related
VeriFixScan crawls a site and applies its checks to every page it reaches, keeping the evidence behind each finding. Scanning one website is free.
Scan a website