Logo
Back to Blog
Web Development August 8, 2026 8 min read

CORS Preflight Requests and the OPTIONS Method Explained

O

OmniWebKit Team

Web Standards

Share:
Article Cover Image

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 didWhy it preflights
Posted JSONapplication/json is not safelisted
Added an Authorization headerNot on the header safelist
Used PUT, PATCH or DELETEMethod outside the simple set
Sent a custom X- headerAnything 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.

Frequently Asked Questions

Why is application/json not a safelisted content type?

+
Because the safelist covers only what an HTML form could already send without JavaScript. Forms can post text, url-encoded data and multipart, so those were already possible before fetch existed. JSON was not, so it needs permission.

Does a preflight carry cookies?

+
No, never, by design. It is a permission question rather than a data request, so it deliberately excludes credentials. That is exactly why auth middleware running on OPTIONS rejects it.

Which request headers are safelisted?

+
Accept, Accept-Language, Content-Language, Content-Type within its allowed values, and a couple of client hints. Everything else, including Authorization and any X-prefixed header, triggers a preflight.

Can I preflight once for a whole API?

+
No, the cache is keyed by URL, method and headers together. Each distinct path re-asks the first time. A high Access-Control-Max-Age reduces repeats but never makes it a single check for the whole service.

How long do browsers actually cache the result?

+
Less than most people set. Chrome caps it at two hours and Firefox at twenty-four, regardless of the value you send. A number in the millions is silently reduced to the cap.

Does the preflight response body matter?

+
Not at all — browsers only read the headers. Returning 204 with no body is the tidiest option. Some older proxies handle 204 badly, so use 200 if you see odd behaviour from something in the middle.

Why does adding one header make a working request start failing?

+
It moved from simple to preflighted. A GET with no custom headers needs no permission check, but adding a tracking header or a token introduces one. Nothing about the GET itself changed.

Do preflights count towards my rate limit?

+
On most gateways, yes, and it is easy to miss. Every non-simple request becomes two, so a rate limit tuned to expected traffic can trip at half the load you planned for.

Is there a way to see the preflight if the network tab hides it?

+
Enable the filter for all requests rather than XHR only, since some tools hide OPTIONS rows by default. A server-side tester is more reliable, because it shows the OPTIONS exchange separately from the real request.

Does HTTP/2 make preflights cheaper?

+
Somewhat, since the extra request shares an open connection rather than opening a new one. The server still has to process it, and on a high-latency mobile connection the extra round trip is still noticeable.

Tags

#CORS#Preflight#HTTP#Web Standards