Disclosure: the fix pages linked here are ours (free, no signup). This article was written with AI assistance and reviewed by us. Not affiliated with any vendor mentioned.
You added Access-Control-Allow-Origin, the methods, the headers. curl works, Postman works, and the browser still says the preflight failed. Here are the three cases we see most where "more headers" won't help, with a curl check for each.
First, the one command that tells you most of what you need. It sends the same OPTIONS preflight the browser sends:
curl -si -X OPTIONS https://api.example.com/api/items \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,authorization"
Look at the status line first, then the access-control-* headers.
1. "Redirect is not allowed for a preflight request"
If the status is 301, 302, 307 or 308, that's your bug. Browsers don't follow redirects on a preflight, so no CORS header fixes it. The usual causes:
- calling
http://when the API redirects tohttps:// - a trailing-slash redirect,
/api/itemsto/api/items/(DjangoAPPEND_SLASH, FastAPIredirect_slashes, Next.jstrailingSlash) - apex to
www, or a login redirect on OPTIONS
Fix: call the exact final URL from your front end (same scheme, host and trailing slash as the Location header). If it's a login redirect, exempt OPTIONS from auth.
2. "It does not have HTTP ok status"
The preflight returned 401, 403, 404, 405 or 500. Preflights never carry cookies or the Authorization header, so auth middleware rejects them, or the router has no OPTIONS handler for that path.
Fix: answer OPTIONS with a 204 before authentication, CSRF and routing:
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
Vary: Origin
3. Wildcard with credentials
If you use fetch(url, { credentials: 'include' }) or axios withCredentials: true, the browser rejects Access-Control-Allow-Origin: *. With credentials, * in Allow-Headers and Allow-Methods is also treated literally, and * never covers Authorization anyway.
Fix: echo the request's Origin only if it's on your allowlist, and add credentials and Vary:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
- Express:
cors({ origin: ['https://app.example.com'], credentials: true }) - FastAPI:
allow_origins=[...](not["*"]) withallow_credentials=True
Don't reflect every Origin blindly. With credentials, that lets any website read your users' authenticated responses. Cross-site cookies also need SameSite=None; Secure.
Bonus: errors that skip your middleware
"No 'Access-Control-Allow-Origin' header is present" often means the request actually failed (a 401, 404 or 500 from an error handler that runs outside your CORS middleware) or a proxy stripped the header. In nginx, use add_header ... always; so 4xx/5xx responses get the header too. In Express, mount the CORS middleware before all routes.
More fixes
We keep a free page for each of these messages, plus the Allow-Headers and Allow-Methods errors: CORS error fixes.
If you'd rather not hand-write this per stack, our $9 CORS Fix Kit includes the cors-doctor CLI, which sends a real preflight and names the wrong header, plus tested configs for 13 stacks.
Top comments (0)