pickuma.
Dev Knowledge

CORS in Plain English: Why the Browser Blocks Your Fetch

A clear walkthrough of CORS and the same-origin policy — what an origin is, why your fetch fails, how servers opt in, and the big misconception about who CORS actually protects.

5 min read

You wrote a fetch(), the server clearly returned data, and yet the console shows a red CORS error and your code never sees the response. The frustrating part is that nothing is broken — the browser is doing exactly what it was designed to do.

What an origin is, and the rule that governs it

An origin is the combination of three things: scheme + host + port. https://app.example.com and http://app.example.com are different origins (different scheme). So are https://example.com and https://api.example.com (different host), and https://example.com and https://example.com:8443 (different port). All three parts must match for two URLs to share an origin.

The same-origin policy is a foundational browser security rule: JavaScript running on one origin is, by default, not allowed to read responses from a different origin. This isn’t about whether the request can be sent — it’s about whether your script is allowed to look at what comes back.

Why does this matter? Imagine you’re logged into your bank in one tab. You open a sketchy site in another. Without the same-origin policy, that site’s JavaScript could quietly call your bank’s API, ride along on your existing session cookies, and read your account data. The same-origin policy is what stops a random page from reading responses meant for you on other sites.

How a server opts in with CORS

The same-origin policy is strict, but plenty of legitimate cross-origin requests exist — a frontend on app.example.com talking to an API on api.example.com, for instance. Cross-Origin Resource Sharing (CORS) is the mechanism that lets a server say “it’s fine for this other origin to read my responses.”

The server does this with response headers. The key one is Access-Control-Allow-Origin:

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

When the browser sees a matching value (or the wildcard *), it permits your JavaScript to read the response. Without it, the browser blocks the read even though the response physically arrived.

For requests that could have side effects or use unusual headers, the browser sends a preflight first: an automatic OPTIONS request that asks the server “are you going to allow a PUT with a Content-Type: application/json header from this origin?” The server answers with Access-Control-Allow-Methods and Access-Control-Allow-Headers. Only if the preflight passes does the browser send the real request. So-called “simple” requests — basic GET/POST with a short list of allowed headers — skip the preflight.

Cookies add another wrinkle. To send credentials (cookies, HTTP auth) on a cross-origin request, the client sets credentials: 'include' and the server must return Access-Control-Allow-Credentials: true. Critically, when credentials are involved you cannot use Access-Control-Allow-Origin: * — the server has to echo back a specific origin. The wildcard and credentials are mutually exclusive by design.

The misconception: who CORS actually protects

Here is the part that trips up most developers. CORS is enforced by the browser, not the server. And it does not protect your server — it protects your users.

A CORS policy can’t stop anyone from calling your API. curl, a Python script, a backend service, Postman — none of them are browsers, so none of them apply the same-origin policy. They send the request, get the response, and read it freely, completely ignoring whatever Access-Control-Allow-Origin header you set. CORS headers are instructions to the browser about what its own JavaScript is allowed to read.

So if you’re thinking of CORS as an access-control or security layer for your backend, that’s the wrong model. Real server protection comes from authentication, authorization, and rate limiting. CORS exists purely to govern what one website’s scripts can read from another inside a user’s browser.

Debugging a CORS error quickly

The fastest sanity check is to take the browser out of the equation:

Terminal window
curl -i https://api.example.com/data

If curl returns the data fine but the browser fails, the issue is purely CORS headers — the server works, it just isn’t telling the browser to allow your origin. From there, inspect the response headers (and the preflight OPTIONS response, if there is one) and confirm Access-Control-Allow-Origin matches your exact origin, scheme and port included.

FAQ

Is CORS a way to secure my API?+
No. CORS only controls what browser JavaScript may read across origins. Non-browser clients like curl, scripts, and other servers ignore it entirely, so it provides zero protection against direct API access. Use authentication, authorization, and rate limiting for actual server security.
Why does my request work in Postman but fail in the browser?+
Postman is not a browser, so it doesn't apply the same-origin policy or honor CORS headers at all. The browser does. A request that succeeds in Postman but fails in the browser almost always means the server is missing or misconfiguring its CORS response headers.
Why can't I use Access-Control-Allow-Origin: * with cookies?+
When a request includes credentials such as cookies, the spec forbids the wildcard origin. The server must echo back a single specific origin and also send Access-Control-Allow-Credentials: true. This prevents any site from making authenticated cross-origin reads using a user's existing session.

Related reading

See all Dev Knowledge articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.