has been blocked by CORS policy: Response to preflight request doesn't pass access control check
A debugging skill for failed CORS preflight requests: why the browser sends an OPTIONS request first, what 'doesn't pass access control check' means (the OPTIONS response is missing allow-origin, allow-methods, or allow-headers), and the server-side fix. Use when browser fetches fail but curl works, or when adding custom headers or credentials to cross-origin calls. Triggers: 'blocked by CORS policy', 'preflight request'. Not for: same-origin fetch errors, CSP violations, or server 500s.
has been blocked by CORS policy: Response to preflight request doesn't pass access control check
TL;DR
The browser sent an OPTIONS preflight to ask permission for your cross-origin request, and the server's answer failed the check, usually because the OPTIONS response is missing the CORS allow headers. Fix it server-side: make your server answer OPTIONS with matching Access-Control-Allow-Origin, Allow-Methods, and Allow-Headers. The fact that curl works proves the endpoint is fine; only the browser enforces this.
has been blocked by CORS policy: Response to preflight request doesn't pass access control checkUse this when
- A browser fetch fails with this error but the same request works in curl or Postman
- You added a custom header, credentials, or a non-simple content type to a cross-origin call
- The preflight OPTIONS request returns 403, 404, or 200 without CORS headers
- A deploy or framework upgrade suddenly broke previously working cross-origin calls
Not for this skill when
- Simple GET requests fail without any preflight (different error, different fix)
- The console shows a Content Security Policy violation instead
- The server itself returns a 500 (fix the server bug first)
Steps
- Confirm it is the preflight failing. In devtools Network tab, find the OPTIONS request to your endpoint. Check its status and response headers.
Expected: the OPTIONS request either errors or returns without Access-Control-Allow-Origin, while a direct curl to the endpoint succeeds.
- See exactly what the preflight asked for and what the server answered:
curl -s -D - -o /dev/null -X OPTIONS https://example.com/api/things -H "Origin: https://app.example" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: authorization,content-type"Expected: you can see which of allow-origin, allow-methods, or allow-headers is missing or mismatched in the response.
- Fix the server to answer OPTIONS properly. Use your framework's CORS middleware (Express
cors(), Django corsheaders, Spring@CrossOrigin), an API gateway CORS config, or web-server headers. The allow-headers list must cover every custom header the browser asked about, and the origin must match when credentials are involved (never*with credentials).
Expected: repeating the step 2 curl shows 200 or 204 with all three allow headers present and correct.
- Retest in the browser with a hard refresh. Preflights can be cached, so confirm with a fresh page load.
Expected: the OPTIONS succeeds and the real request fires immediately after.
Variant: preflight returns 403 from a WAF or auth layer
Security middleware often runs before CORS middleware and rejects OPTIONS before the CORS headers are added. Move CORS handling ahead of auth, or explicitly allow OPTIONS through unauthenticated.
Variant: "Response to preflight ... doesn't pass" with credentials
With credentials: 'include', the server must echo your exact origin in allow-origin and include allow-credentials: true. A wildcard origin is rejected by the browser here, even though the preflight itself returned 200.
Variant: custom headers triggering preflight
Any header outside the safelist (like Authorization or X-Requested-With) triggers a preflight. Either add it to allow-headers server-side or stop sending it if it is not needed.
Variant: API Gateway or serverless CORS config
Managed gateways have their own CORS toggles that generate the OPTIONS response for you. If you set headers in code but the gateway also manages CORS, the two can conflict. Pick one place to own it.
Why this happens
For "non-simple" cross-origin requests, the browser first asks the server for permission with an OPTIONS request describing the method and headers it wants to use. The server must answer with explicit allow headers. If the answer is missing or wrong, the browser blocks the real request without ever sending it. curl skips this entirely, which is why the endpoint looks healthy everywhere except the browser.
Edge cases and pitfalls
- Browsers cache successful preflights (Access-Control-Max-Age). A fixed server can look broken until the cached failure expires. Test in a fresh profile to be sure.
- Redirects on the OPTIONS request fail the check. The preflight URL must answer directly, not 301 elsewhere.
- Proxies and CDNs sometimes strip OPTIONS responses or their headers. Check what actually reaches the browser, not just what your app sent.
- Do not "fix" this by routing around the browser check with a proxy unless you understand you are moving the trust boundary. Usually the right fix is proper server headers.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_fVHqKDVJhsEWi1YmGyLcnA
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.