You watch the network tab and see two requests where you wrote one. The first is an OPTIONS you never sent, and it arrived before your POST. Nothing is broken — the browser is asking permission, and knowing when it does that explains a lot of otherwise baffling behaviour.
What a CORS Preflight Request Is
A permission check the browser sends before certain cross-origin requests.
It uses the OPTIONS method and carries no body and no credentials. It describes what the real request intends to do and waits for approval.
Approve it and the real request follows. Refuse it and the real request never happens at all.
You can watch both halves separately in our CORS tester, which sends the OPTIONS exchange on its own.
Simple vs Preflight Request: What Decides
A request is simple only if it could have been made by an HTML form before JavaScript existed.
That historical rule explains every condition in the list. Three things must all hold:
- Method is GET, HEAD or POST.
- Headers come only from the safelist: Accept, Accept-Language, Content-Language, Content-Type, and a few client hints.
- Content-Type is text/plain, multipart/form-data or application/x-www-form-urlencoded.
Break any one and you get a preflight.
Notice what is missing from that content type list: application/json. Sending JSON always triggers a check, which is why almost every modern API call is preflighted.
When Is Preflight Triggered in Practice
Four everyday things that cross the line.
| What you did | Why it preflights |
|---|---|
| Posted JSON | application/json is not safelisted |
| Added an Authorization header | Not on the header safelist |
| Used PUT, PATCH or DELETE | Method outside the simple set |
| Sent a custom X- header | Anything non-safelisted counts |
The second row causes a specific kind of confusion. A GET that worked for months starts failing the day someone adds a bearer token, and the GET itself did not change.
What the Browser Sends and Expects Back
Three request headers out, three response headers in.
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type
Your server has to answer all three questions:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
The status code barely matters — 200 and 204 both work, and browsers read only the headers. What matters is that every requested method and header appears in the allow lists.
A caveat worth knowing: the response body is ignored entirely, so there is no point returning a helpful error message in it.
The OPTIONS Method HTTP Cache
Access-Control-Max-Age tells the browser how long it may skip re-asking.
Access-Control-Max-Age: 86400
With that set, the browser caches the approval and sends the real request directly next time. On a chatty app that removes a great many round trips.
Two limits stop it being a silver bullet. The cache is keyed by URL, method and headers together, so a different endpoint re-asks. And browsers cap the value — Chrome at two hours, Firefox at twenty-four, whatever number you send.
One more thing teams miss: preflights usually count against API rate limits. Every non-simple call becomes two requests, so a limit tuned for expected traffic can trip at half the load.
Avoiding the Extra Round Trip
Sometimes you can stay inside the simple rules, and often you should not try.
Sending form-encoded data instead of JSON keeps a POST simple. So does moving a token from a header into a cookie.
We rarely recommend either. JSON bodies and Authorization headers exist for good reasons, and contorting your API to dodge one cached OPTIONS request is a poor trade.
The better lever is Max-Age. Set it sensibly and the cost disappears after the first call.
To see exactly which headers your endpoint returns, our HTTP headers checker shows the full response.
Wrapping Up
A CORS preflight request is the browser asking before it acts. JSON bodies, custom headers, and methods beyond the basic three all trigger one.
Answer with the allow lists your app actually needs, set a sensible Max-Age, and remember the preflight carries no credentials.
If yours is failing rather than merely puzzling, the OPTIONS troubleshooting guide walks through the five causes. For the wider model, see what CORS is and why it exists.
