CORS Preflight Failed — Browser Blocks the Request
Diagnose preflight failures by inspecting the OPTIONS request and response headers, then fix origin, method, header, or credential configuration.
Symptoms
- Console reports 'Response to preflight request doesn't pass access control check'
- The Network panel shows an OPTIONS request with 404, 405, or 500 and no actual request follows
- curl succeeds while the browser request fails, which points to a browser-enforced CORS check
Possible causes
- The server does not handle OPTIONS preflight requests or returns an error
- The request origin is not in Access-Control-Allow-Origin
- Access-Control-Allow-Methods or Access-Control-Allow-Headers omits the method or custom header
- Credentials are used with a wildcard origin or without Access-Control-Allow-Credentials
Diagnosis steps
Reproduce the preflight with curl
curl -i -X OPTIONS https://api.example.com/orders -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type'Interpretation
Inspect the status and headers. A 2xx without Access-Control-Allow-Origin still fails the browser check; a 404/405 means OPTIONS is not routed.
Compare the origin in the request and response
grep -i 'access-control\|vary' <(curl -sI https://api.example.com/orders)Interpretation
The allowed origin must match exactly, including scheme and port. A wildcard is invalid when credentials are included.
Check the browser Network panel for the actual request
DevTools → Network → filter by Fetch/XHR → inspect the OPTIONS entryInterpretation
If the preflight fails, the browser never sends the real request. Fix the preflight first.
Root cause
The browser sends an OPTIONS request before certain cross-origin requests and requires the response to explicitly allow the origin, method, and headers. If any required header is missing or the OPTIONS route fails, the browser blocks the actual request before it is sent.
Fix
Handle OPTIONS explicitly, return 204 with Access-Control-Allow-Origin set to the exact allowed origin, list the required methods and headers, add Vary: Origin, and use exact origins with Access-Control-Allow-Credentials when cookies are involved.
Risk note
Reflecting every origin or allowing null origins disables the protection. Do not disable CORS checks in production as a workaround.
Verify the fix
Re-run the preflight and confirm 204 with the expected headers, then confirm the actual POST succeeds in the browser. Test a disallowed origin to ensure it is still blocked.
The browser fails before your handler runs
For non-simple requests, the browser first sends OPTIONS with Access-Control-Request-Method and Access-Control-Request-Headers. If that response does not satisfy the CORS rules, the actual request is never sent. This is why server logs show only an OPTIONS request, or nothing at all.
curl -i -X OPTIONS https://api.example.com/orders \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
A valid response should include the exact origin, the allowed method, and the allowed headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type
Vary: Origin
Fix and verify
Route OPTIONS before authentication middleware, because preflight requests do not carry cookies or authorization headers. Keep the allowlist explicit. After the fix, the browser should send the real request and receive a normal response. Verify that an unlisted origin still gets no CORS headers; otherwise you have replaced the allowlist with a reflection.
Common traps
Access-Control-Allow-Origin: * cannot be combined with credentials. A proxy may strip headers. A redirect during preflight is not allowed by many browsers. Check the final response after redirects, not only the first hop.