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