CORS, explained: why the browser keeps blocking your requests
July 12, 2026
What Cross-Origin Resource Sharing actually is, why browsers enforce it, and how a preflight works — with diagrams.

If you've ever built a web app that talks to an API, you've probably met this error:
"…has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
It feels like the browser is being difficult. It's actually protecting your users. Here's what's going on.
What is an "origin"?
An origin is the combination of scheme + domain + port:
https://myblog.com:443
──┬── ────┬──── ─┬─
scheme domain port
Two URLs are "same-origin" only if all three match. These are all different origins:
https://myblog.com ← the site
https://api.myblog.com ← different domain
http://myblog.com ← different scheme
https://myblog.com:8081 ← different port
The problem CORS solves
You're logged in to your bank. Your browser holds a session cookie for mybank.com. Then you visit evil-site.com, which quietly runs:
fetch("https://mybank.com/api/transfer?to=attacker&amount=5000")
Browsers attach cookies automatically — so without protection, that request would arrive at your bank authenticated as you. To prevent this, browsers enforce the same-origin policy: JavaScript on one origin can't read responses from another origin.
SAME-ORIGIN POLICY
evil-site.com (JS) mybank.com
│ │
│ fetch("mybank.com/api/…") │
├─────────────────────────────────────▶│
│ │
│ response │
│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│ │
✗ BLOCKED by the browser:
the response never reaches evil-site's JavaScript
But that rule is too strict: sometimes cross-origin calls are exactly what you want — like a frontend on myblog.com calling its own API on api.myblog.com. That's where CORS comes in.
CORS: a controlled exception
Cross-Origin Resource Sharing lets a server declare which foreign origins may read its responses. The browser asks; the server answers with headers.
SIMPLE CROSS-ORIGIN REQUEST
myblog.com (JS) api.myblog.com
│ │
│ GET /api/posts │
│ Origin: https://myblog.com │
├─────────────────────────────────────▶│
│ │
│ 200 OK │
│ Access-Control-Allow-Origin: │
│ https://myblog.com ────┼── "myblog.com may read this"
│◀─────────────────────────────────────┤
│ │
✓ ALLOWED: header matches the page's origin,
so the browser hands the response to JavaScript
If the Access-Control-Allow-Origin header is missing or names a different origin, the browser throws the CORS error you know and love. Note the request still reached the server — CORS controls whether the response is readable, not whether the request is sent.
The preflight: asking permission first
For anything beyond simple GETs and form posts — a PUT, a DELETE, a JSON Content-Type, an Authorization header — the browser first sends a scout: an OPTIONS request called a preflight.
PREFLIGHT FLOW
myblog.com (JS) api.myblog.com
│ │
│ ① OPTIONS /api/posts │
│ Origin: https://myblog.com │
│ Access-Control-Request-Method: │
│ PUT │
├─────────────────────────────────────▶│
│ │
│ ② 200 OK │
│ Access-Control-Allow-Origin: │
│ https://myblog.com │
│ Access-Control-Allow-Methods: │
│ GET, POST, PUT, DELETE │
│◀─────────────────────────────────────┤
│ │
│ ③ the real PUT /api/posts │
├─────────────────────────────────────▶│
│ ④ 200 OK + data │
│◀─────────────────────────────────────┤
If step ② doesn't approve the method, origin, or headers, the browser never sends the real request at all.
Why this matters in practice
This very blog runs as two services: the site you're reading and an API on a different subdomain. Without the API replying Access-Control-Allow-Origin: <the site's origin>, the admin dashboard couldn't log in — the browser would block every call. Uploads go straight from the browser to cloud storage, so the storage bucket needs its own CORS policy too.
Three things worth remembering:
- CORS is browser-enforced.
curl, Postman, and server-to-server calls ignore it entirely. It protects users, not your API — you still need real authentication. - The server allows; the browser enforces. Fixes always belong on the server you're calling, never in your frontend code.
Access-Control-Allow-Origin: *is a smell on anything private — it means "any website may read this response." Fine for public data, dangerous with credentials (and browsers refuse the combination outright).
Next time the console yells about CORS, you'll know: the browser isn't broken. It's asking the server, "are you sure you two know each other?"
Comments
No comments yet — be the first.