CORS Explained: Why the Browser Blocked Your Request

October 2, 2026 · 3 min read

Access to fetch at 'https://api.example/me' from origin 'https://app.example' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Almost every web developer meets this message. It's frustrating because it seems to block a perfectly reasonable request — but it's protecting your users from something real.

The same-origin policy

An origin is the scheme, host and port of a URL:

https://app.example:443/dashboard
└─────────┬─────────┘
       origin

https://app.example and https://api.example are different origins. So are http://app.example and https://app.example, and localhost:3000 and localhost:8080.

The browser's same-origin policy says: a page's JavaScript may send requests anywhere, but it may only read responses from its own origin.

Why that rule exists

Imagine you're logged in to your bank in one tab, and you visit a malicious site in another. Its JavaScript runs:

const res = await fetch('https://bank.example/api/transactions');
const data = await res.json();   // your private transactions?

The browser would attach your bank cookies automatically. Without the same-origin policy, any site you visit could read any other site's data as you. That's what CORS is guarding: your data on other sites, from the page you're currently on.

CORS: how a server opts in

CORS (Cross-Origin Resource Sharing) lets a server say "this other origin may read my responses" by sending headers.

app.example page
browser
api.example

A page on app.example wants data from api.example. The two have different origins (scheme + host + port), so the browser applies the same-origin policy — and CORS is the way a server relaxes it.

0 / 10

For a simple request — a GET, HEAD or basic POST with no custom headers — the browser sends it with an Origin header and then checks the response:

Access-Control-Allow-Origin: https://app.example

If the header allows the page's origin, the page gets the response. If not, the browser hides it and logs the error. Notice that the request still reached the server — CORS isn't a firewall.

Preflight requests

For anything that could have side effects a plain HTML form couldn't already cause — PUT, DELETE, a JSON body, a custom header like Authorization — the browser first sends a preflight: an OPTIONS request asking permission.

OPTIONS /me HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

The server answers with what it allows:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, PUT
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600

Only if that matches does the browser send the real PUT. Max-Age lets it skip the preflight for a while.

Cookies need an extra opt-in

By default, cross-origin fetch doesn't send cookies. To include them, the client asks with credentials: 'include' and the server must respond with both:

Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true

And in this case the allowed origin can't be the wildcard * — it has to name the exact origin. That rule exists to stop servers accidentally exposing logged-in data to every site on the internet.

Fixing CORS errors

The fix always goes on the server being called. Nothing in the calling page can grant itself permission — that would defeat the purpose.

  • You control the API: send Access-Control-Allow-Origin for the origins you trust, and answer OPTIONS preflights. Most frameworks have a CORS middleware that does both.
  • You don't control the API: call it from your own server instead (a small proxy route). Server-to-server requests have no same-origin policy — it's a browser rule.
  • Local development: use your dev server's proxy setting so the front end and API appear to share one origin.

What doesn't work: mode: 'no-cors'. It sends the request but gives you an "opaque" response you can't read — it silences the error without giving you the data.

Common mistakes

  • Returning Access-Control-Allow-Origin: * on an API that uses cookies or returns private data.
  • Reflecting any incoming Origin straight back into the header — the same as allowing everyone. Check it against an allowlist.
  • Forgetting the preflight route, so OPTIONS returns 404 and every PUT fails.

CORS errors feel like the browser getting in your way. They're really the browser asking the API one question on your users' behalf: do you trust the site that's asking?