What CORS Errors Actually Mean (And How to Fix Them)
By Sarfaraz Khan ·
If you've ever seen Access to fetch at '...' from origin '...' has been blocked by CORS policy in
the console and immediately reached for a workaround involving mode: "no-cors", this post is for
you — that "fix" doesn't actually fix anything, it just hides the response from your own code.
The request usually already worked
The single most confusing thing about CORS is this: the request almost always succeeds. Open
your Network tab and you'll usually see a 200 sitting right there, response body and all. CORS
isn't a network failure — it's the browser deciding, after the fact, whether your JavaScript is
allowed to read a response that already arrived. The request left, the server answered, and then
the browser stepped in between the response and your fetch() call and said no.
That's why curl or Postman "work fine" for the exact same request that fails in the browser: neither of them enforces CORS at all. It's a browser-only security boundary, full stop. If you want to see the request itself the way the browser sees it — headers, method, whether it's a preflight — a cURL → Code Converter is a fast way to turn what you copied from DevTools into something you can actually inspect line by line.
What the browser is checking
For a "simple" request (GET, or POST with a plain content type), the browser sends the request, gets
the response, and then checks one header: Access-Control-Allow-Origin. If it's missing, or set to
a different origin than the page making the request, the browser throws the response away before
your code ever sees it.
For anything else — a custom header, a PUT/DELETE, Content-Type: application/json on some
older server configs — the browser first sends a preflight: an OPTIONS request asking "if I sent
the real request, would you allow it?" Your server has to answer that OPTIONS request correctly
(status 204 or 200, the right Access-Control-Allow-* headers) before the real request is even
attempted. A shockingly common bug is a server that handles the real request fine but has no route
for OPTIONS at all, so the preflight itself 404s or 405s and the real request never goes out.
This is exactly the kind of thing that's hard to see just by staring at a failed fetch — you need to
know whether you're looking at a failed preflight or a failed real request, and what header was
actually missing. That's what a CORS Debugger is for: it fires the actual
request, shows you whether a preflight happened, and tells you specifically which
Access-Control-Allow-* header would need to change for the browser to accept the response.
The actual fixes, in order of how often they're the real problem
- The server isn't sending
Access-Control-Allow-Originat all. Add it, matching the calling origin exactly (or*if the endpoint is genuinely public and doesn't use credentials). - You're sending cookies (
credentials: "include") but the server sendsAccess-Control-Allow-Origin: *. Wildcard origins are explicitly incompatible with credentialed requests — the server has to echo back the specific origin, and also sendAccess-Control-Allow-Credentials: true. - A custom header (like
Authorizationor an API key header) isn't inAccess-Control-Allow-Headerson the preflight response. The browser checks this list literally; a header your server accepts on the real request but forgot to list on the OPTIONS response still gets blocked. - Something in front of your API — a CDN, a load balancer, an API gateway — is stripping the headers your app code correctly sends. This one's sneaky because your server logs show the right headers going out, but they never reach the browser. If you've confirmed the headers are correct at the app layer and it's still failing, check every hop between your server and the internet, not just your own code. An HTTP Status Code & Header Reference is useful here for double-checking exactly which header name and casing the spec expects, since a typo'd header name fails silently — the browser doesn't warn you "close, but not quite."
None of this requires disabling CORS checks in your browser for development, and please don't get in the habit of doing that — it trains you to "fix" the symptom instead of the one header that was actually missing.