Preflight request
A preflight is an OPTIONS request the browser sends before a non-simple cross-origin request, asking the server whether the real request is allowed. The real request only goes out if the preflight response says yes.
When the browser preflights
Any cross-origin request that isn't a "simple" GET/POST/HEAD with a safe Content-Type. In practice you'll see a preflight whenever:
- The method is anything other than
GET,HEADorPOST, for examplePUT,DELETEorPATCH. (Fetch forbidsCONNECT,TRACEandTRACKoutright, so those never get as far as a preflight.) - The request carries an
Authorizationheader, a customX-*header, or any header outside the CORS-safelist. - The
Content-Typeis anything other thanapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain, soapplication/jsonalways triggers one.
What it looks like on the wire
// Browser → server
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
// Server → browser (says yes, with which origin and methods are allowed)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
// Only then does the real POST go out:
POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer …
Content-Type: application/json
…
Why it exists
Older HTTP APIs (forms, image embeds, GETs) were always allowed cross-origin; the web was built that way. CORS adds new capabilities (custom headers, non-trivial methods) without breaking that legacy: anything new and potentially destructive requires explicit per-origin opt-in via the preflight.
Why preflights make CORS errors confusing
A preflight failure looks identical to a "regular" CORS failure in the console: "blocked by CORS policy". But the request that failed is the OPTIONS, not your real call. If you only check the failed POST in the Network tab, you'll miss the OPTIONS right before it that's actually the problem.
Practical debugging: in DevTools, open the Network tab, filter by "Other" or by status code, find the OPTIONS, and look at its response headers. The missing piece will usually be Access-Control-Allow-Methods or Access-Control-Allow-Headers not including what your real call needs.
Reducing preflight overhead
Preflights add a round-trip before the real request. The browser can cache the answer for up to Access-Control-Max-Age seconds, and each browser applies its own upper limit. corsproxy.dev answers preflights itself and sends Access-Control-Max-Age: 86400. See our deep dive on preflight caching for the per-browser caps, the cache key, and what invalidates it.