Domain.Posture
← blog

CORS preflight requests: why they fail and how to fix them

· cors · http · browsers · api

corshttpbrowsersapi

CORS is a browser feature. The server is a participant but not the enforcer — it only advertises permissions via headers, and the browser decides whether to let JavaScript read the response. The preflight is the browser asking "before I send the real request, will you accept it?"

What triggers a preflight

A browser sends an OPTIONS preflight before a cross-origin fetch() or XMLHttpRequest when any of the following hold:

  1. The HTTP method is anything other than GET, HEAD, or POST.
  2. The request has a custom header — anything outside the "CORS-safelisted request headers" (Accept, Accept-Language, Content-Language, Content-Type, Range).
  3. The Content-Type is not application/x-www-form-urlencoded, multipart/form-data, or text/plain. In particular, application/json triggers a preflight.
  4. The request body is a ReadableStream.

Plain GET to a public JSON endpoint without custom headers? No preflight — the browser sends it directly. POST with Content-Type: application/json? Preflight first.

The preflight request

OPTIONS /api/v1/things HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

The browser declares: "I'm at origin X, I want to send method Y with headers Z. Will you accept?"

The server response

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin
  • Access-Control-Allow-Origin must echo the Origin header exactly, or be * (which disallows credentialed requests — see below).
  • Access-Control-Allow-Methods lists methods the server accepts.
  • Access-Control-Allow-Headers must include every header the browser asked about.
  • Access-Control-Allow-Credentials: true is required if the real request will include cookies or an Authorization header. Only valid with a specific origin, never *.
  • Access-Control-Max-Age caches the preflight result for N seconds so the browser doesn't preflight every single request.
  • Vary: Origin is essential if the server varies Allow-Origin per origin — without it, a CDN caches the first response and serves it to every origin.

Five failures you will actually see

1. Access-Control-Allow-Origin is * but the request is credentialed. The browser console reads: "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '' when the request's credentials mode is 'include'."* Fix: echo the specific origin.

2. Missing Access-Control-Allow-Headers entry. You added Authorization to the client fetch, the server forgot to list it. The browser refuses to send the real request. Fix: add the header to the allow list.

3. CDN caches the preflight without Vary. First browser from origin A gets a response with Allow-Origin: A. Second browser from origin B gets the same cached response, sees Allow-Origin: A, fails. Fix: Vary: Origin on every preflight response.

4. The origin is not the one you think. Mobile apps, Chrome extensions, and cross-tab iframes all have origins that may not match what you whitelisted. Check the real Origin header before blaming the server config.

5. Preflight returns 4xx. Often a misconfigured route guard (auth middleware) rejecting the OPTIONS before CORS middleware runs. Fix: allow OPTIONS through authentication-free, or move the CORS middleware earlier in the chain.

Run a CORS preflight against any domain →

Further reading

Frequently asked questions

When does a browser send a CORS preflight request?
A preflight (OPTIONS request) is sent for cross-origin fetches when: the method is not GET/HEAD/POST, custom headers are included, Content-Type is application/json or not form-encoded, or the body is a ReadableStream.
What does the server need to return in a CORS preflight response?
The server must return Access-Control-Allow-Origin (specific origin or *), Access-Control-Allow-Methods, Access-Control-Allow-Headers (listing all requested headers), and Vary: Origin to prevent CDN caching issues.
Why does my CORS request fail with wildcard origin and credentials?
The browser blocks requests with credentials (cookies, Authorization headers) when Access-Control-Allow-Origin is *. You must echo the specific origin instead of using a wildcard.
How can a CDN cause CORS preflight failures?
Without Vary: Origin in the preflight response, a CDN caches the response for the first origin and serves the same cached response to other origins, causing failures for subsequent requests from different origins.
Why does my preflight request return a 4xx error?
This typically happens when an authentication middleware rejects the OPTIONS request before CORS middleware runs. Move CORS middleware earlier in the chain or bypass authentication for OPTIONS requests.