CORS Explained: Why Your Fetch Request Failed

Published July 26, 2026By Samson PG

The request usually succeeds on the server — the browser just refuses to hand the response to your JavaScript. CORS is a browser rule, not a network failure, and the fix is almost always on the server, not your fetch call.

CORS (Cross-Origin Resource Sharing) is the single most common reason a fetch() that works in Postman, curl, or a server-side script fails in the browser with something like “has been blocked by CORS policy.” The confusing part: the server usually did process the request and send a response — the browser is the one refusing to let your JavaScript read it.

Why this exists at all

By default, browsers enforce the same-origin policy: a script running on https://app.example.com can’t read responses from https://api.other.com unless that other origin explicitly says it’s allowed to. This isn’t a bug or a network problem — it’s a deliberate security boundary that stops a malicious script on one site from silently reading your logged-in data on another (your bank, your email) using your browser’s own cookies.

CORS is the mechanism that lets a server opt back in: it responds with headers like Access-Control-Allow-Origin that tell the browser “yes, app.example.com is allowed to read this.”

What actually happens on the wire

For a simple GET/POST with common headers, the browser sends the real request and just checks the response headers before handing the body to your code. For anything less “simple” — a custom header like Authorization, a Content-Type: application/json body, or a method like PUT/DELETE — the browser first sends a preflight: an OPTIONS request asking the server “would you allow this?” before it ever sends the real one. If the server doesn’t answer the preflight correctly, your actual request never goes out at all — which is why the failure can look like nothing happened, when really an OPTIONS request failed silently in the background.

Reading the actual error

  • “No ‘Access-Control-Allow-Origin’ header is present” — the server didn’t send a CORS header at all. Fix: the server needs to add one (or a proxy/gateway in front of it does).
  • “…does not match the supplied origin” — the server sent an allow-list, and your origin isn’t on it.
  • Preflight fails (OPTIONS request errors or 404s) — the server doesn’t handle OPTIONS requests correctly for that route; common with API frameworks that need CORS middleware explicitly enabled.
  • Works with credentials: 'omit' but not 'include' — sending cookies/auth cross-origin requires the server to send Access-Control-Allow-Credentials: true, and critically, Access-Control-Allow-Origin cannot be * in that case — it must echo back your exact origin.

What you can’t fix from the frontend

CORS headers are set by the server you’re calling, not the page making the request. No fetch() option, no library, and no browser extension in a shipped product can add permission that the server didn’t grant — anything that appears to “fix” it client-side (like a public CORS-proxy service) is routing your request through a third party, which has its own trust and reliability trade-offs. If you control the API, add the header there. If you don’t, you need the API owner to allow your origin, or a backend of your own to proxy the request server-to-server (where CORS doesn’t apply — it’s a browser-only rule).

FAQ

Does CORS block the request, or just the response?

The request is usually sent (for “simple” requests) and the server processes it — CORS blocks your JavaScript from reading the response. For requests that need a preflight, the real request may never be sent if the preflight itself fails.

Why does it work in Postman/curl but not the browser?

Because CORS is a browser-enforced rule, not a server-side or network-level restriction. Tools like curl and Postman aren’t browsers and don’t apply same-origin policy at all — they’ll happily read any response.

Is Access-Control-Allow-Origin: * a security risk?

It means any origin can read the response — fine for public, non-authenticated data (like a public API), but it must never be combined with Access-Control-Allow-Credentials: true, and browsers actively forbid that combination.

My server sends the header but it still fails — why?

Check for a caching layer (CDN, reverse proxy) stripping or overriding the header, multiple Access-Control-Allow-Origin values being sent (only one is valid), or the preflight OPTIONS route not being handled at all.

← More from the blog

TryDevSnip