Safeguard
Developer Tools•

What triggers a CORS preflight request, and how to cache it

A preflight is an OPTIONS check the browser sends before some cross-origin requests. Learn what triggers one, what the server must answer and how caching works.

Safeguard Security Team
3 min read

A browser sends a CORS preflight, an OPTIONS request, before any cross-origin request that is not "simple". Methods other than GET, HEAD or POST, a custom or Authorization header, or a JSON content type all trigger one. The server's reply decides whether the real request is sent, and Access-Control-Max-Age controls how long that answer is cached.

What counts as a simple request

A request skips the preflight only if all of these hold, per MDN:

  • The method is GET, HEAD or POST.
  • Apart from headers the browser sets itself, only safelisted headers are set: Accept, Accept-Language, Content-Language, Content-Type (with limits) and Range with a single range.
  • If there is a Content-Type, it is application/x-www-form-urlencoded, multipart/form-data or text/plain.
  • No upload event listeners on an XMLHttpRequest, and no ReadableStream body.

That list is why a fetch that posts application/json triggers a preflight, even though the method is POST. It is also why adding an Authorization header does.

What the exchange looks like

The browser sends OPTIONS with Access-Control-Request-Method and, when relevant, Access-Control-Request-Headers. The server answers with what it allows:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Only then does the browser send the real request. Preflights never carry credentials. If the real request will include cookies, the preflight response must also say Access-Control-Allow-Credentials: true.

Caching with Max-Age

Access-Control-Max-Age gives the number of seconds the browser may reuse the preflight answer. MDN states the default is 5 seconds, which means a busy page may preflight constantly. Setting it to 86400 allows a day. Each browser enforces its own internal maximum, which takes precedence when your value is higher. MDN does not give the cap numbers, so test in the browsers you support rather than assuming.

Longer caching has a cost. If you tighten the allowed headers, clients that cached the old answer may keep using it until it expires.

Common mistakes

  • Handling OPTIONS behind authentication, so the preflight gets a 401. Preflights carry no credentials.
  • Forgetting a header in Access-Control-Allow-Headers, which fails only for the requests that use it.
  • Treating the preflight as a security control. It protects browsers from sending certain requests, but a script outside a browser ignores it, so your server must still authorise every request.

A debugging habit

Open the network panel, find the OPTIONS request, and compare the request headers with the response. The browser console error usually names the exact header or method that was not allowed. Fix that single item rather than widening everything.

Quick check

Why does my JSON POST send a preflight?

Because application/json is not one of the three safelisted content types, the request is not simple.

Does a preflight protect my API?

No. It is a browser behavior. Any non-browser client can skip it, so authorise on the server.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.