Advertisement

Few messages generate as much frustration as a CORS error blocking a request that "should just work". The instinct is to see the browser as being obstinate. In reality, CORS is a deliberate security mechanism, and the error means it is protecting users exactly as designed. Understanding what it guards against turns the error from infuriating to informative.

The starting point is a rule the whole web is built on.

The same-origin foundation

By default, browsers enforce a same-origin policy: a page can freely talk to its own origin but is restricted from freely reading responses from a different origin. This exists to stop a malicious page from quietly making authenticated requests to sites you are logged into and stealing the results. Without it, the web would be far more dangerous.

CORS is the controlled way to relax this restriction for cases where cross-origin access is legitimately wanted.

Advertisement

What CORS actually is

Cross-Origin Resource Sharing is a system where a server can declare, via specific response headers, that it permits requests from certain other origins. The browser checks these headers and allows the cross-origin response through only if the server has explicitly opted in. So a CORS error almost always means the server has not granted permission for that origin — not that the browser is broken.

For some requests, the browser even sends a preliminary "preflight" check to ask the server whether the real request is allowed, before making it.

Fixing it correctly

Because CORS is enforced by the browser but controlled by the server, the fix belongs on the server: configuring it to permit the origins that genuinely need access, and only those. The tempting shortcut of allowing every origin indiscriminately defeats the security purpose and should be avoided, especially for anything sensitive.

Read the error as a message from a working security feature: "this server has not said your origin is allowed". Then grant the permission deliberately, narrowly, on the server — and the mystery evaporates.

Advertisement

The same-origin policy CORS exists to carefully relax

CORS only makes sense in light of the rule it is an exception to: the same-origin policy, a browser default that blocks a script running on one origin from reading responses from a different origin — different scheme, host, or port counts as different — even if the request itself is technically allowed to go out over the network. This default exists because a browser routinely holds authenticated sessions with many sites at once via cookies, and without this restriction, a malicious page could quietly make requests to a bank's API using the visitor's already-logged-in session and read the response back into its own script, which is exactly the kind of cross-site attack the same-origin policy was created to prevent in the first place.

CORS is the mechanism that lets a server deliberately opt out of this default restriction for specific origins it trusts, by attaching response headers that tell the browser 'it is fine for a script running on this other origin to read this response.' Without any such header, the browser's default is to block the read entirely — the request may well have reached the server and gotten a response, but the browser refuses to hand that response to the calling script, which is precisely why so many CORS errors show a network request that appears to succeed in the network tab while the calling code still receives nothing.

Simple requests versus the preflight

Not every cross-origin request is treated the same way. A 'simple request' — a GET, HEAD, or POST using only a small allow-listed set of headers and content types — is sent directly, and the browser only checks the CORS response header after the fact to decide whether to expose the response to the calling script. Anything outside that narrow definition — a PUT or DELETE, a custom header like an auth token, a JSON content type — triggers a preflight: the browser sends an OPTIONS request first, asking the server to confirm in advance which methods, headers, and origins are actually allowed, and only proceeds with the real request if the server's preflight response grants permission.

This is why adding one custom header to an otherwise simple fetch call can suddenly introduce a CORS failure that was not there before — it silently converts a simple request into a preflighted one, and the OPTIONS response now has to explicitly grant that specific header for the follow-up request to be allowed to proceed, which teams unfamiliar with the distinction often discover only when a previously working endpoint starts failing after a client-side change that looked unrelated to CORS at all.

Advertisement

The wildcard trap: `*` and credentials do not mix

`Access-Control-Allow-Origin: *` is the fastest way to make a CORS error disappear, and it is also frequently the wrong fix, because a wildcard origin combined with credentialed requests — cookies, HTTP authentication — is explicitly disallowed by the specification and browsers enforce this by refusing the request rather than silently ignoring the mismatch. A server that needs to support credentialed cross-origin requests has to echo back the specific requesting origin rather than a wildcard, verified against an actual allow-list, which is more code than a blanket wildcard but is also the only way to support authenticated cross-origin access at all, and is a meaningfully more secure default besides, since a wildcard combined with a looser reading of the spec would otherwise let literally any site read authenticated responses.

Why the fix almost always lives on the server, not the client

A CORS failure is fundamentally the server declining, by omission, to grant permission for a cross-origin read, which means no amount of client-side code changes can fix it — the browser is enforcing a restriction the server has not lifted, and trying to work around it purely from the frontend (retrying, changing fetch options, catching the error and ignoring it) addresses none of the actual cause. The one genuine client-side lever is a proxy: routing the request through a same-origin server endpoint that then makes the actual cross-origin call itself, sidestepping the browser's CORS enforcement because the browser only ever talks to the same origin — but this is a workaround for cases where the actual third-party server cannot be configured, not a substitute for configuring CORS correctly when the server is under the team's own control.

CORS is not a security boundary for the server itself

A common and consequential misunderstanding is treating a permissive CORS configuration as harmless because 'the API still requires authentication.' CORS governs which origins a browser will let read a response, but it says nothing about which origins may make the request in the first place — a request without the right CORS headers still reaches the server and still executes, and any side effect it caused already happened, regardless of whether the browser later lets the calling page see the response. Any endpoint that changes state has to be protected by real authentication and authorization checks on the server, exactly as it would if CORS did not exist at all; CORS only ever controls read visibility of the response in the browser, never whether the underlying request itself was allowed to happen.

Why the browser enforces this and the server cannot opt out unilaterally

It is worth being precise about where CORS enforcement actually happens: entirely inside the browser, not on the network or at the server. A server has no way to force a browser to expose or withhold a response from calling script; all it can do is attach headers stating its own preference, and the browser is the party that actually reads those headers and decides whether to honor the calling script's read request. This is exactly why CORS only matters for browser-based clients — a server-to-server request, or a request made from a command-line tool like curl, is never subject to CORS at all, because there is no browser in the loop enforcing the same-origin policy in the first place, and the restriction only ever applied to that one specific client.

Vary: Origin — the caching header CORS quietly depends on

A server that returns different CORS headers depending on the requesting origin — echoing back whichever specific origin made the request rather than a single fixed value — has to also send `Vary: Origin` on that response, telling any caching layer sitting in front of it (a CDN, a shared proxy) that the response content varies depending on the origin header and must not be cached and served to a different origin's request without revalidating. Omitting this header in front of a caching layer can produce a genuinely dangerous bug: origin A's CORS-permitted response gets cached and then served to origin B's request as though it were also permitted for B, silently defeating the entire access-control intent of having origin-specific CORS headers in the first place.

Credentials and cross-subdomain requests still count as cross-origin

It is a common surprise that `app.example.com` and `api.example.com` are different origins as far as the browser is concerned, even though both are under the same parent domain and often controlled by the same team — the same-origin policy compares scheme, full host, and port exactly, with no special exception for sharing a parent domain, which means the entire CORS mechanism described throughout this article applies in full to a request between two subdomains of the same site just as it would between two entirely unrelated domains.

Why localhost development often hides a CORS problem until deploy

It is common for a frontend and backend running on the same machine during development, sometimes even on the same port through a dev-server proxy, to never trigger a CORS check at all during local work, only for the exact same code to fail once deployed to separate production origins — which is why explicitly testing cross-origin behavior against real, separately hosted origins before shipping is worth doing deliberately rather than trusting that a clean local development experience says anything reliable about CORS behavior in production.

Custom domains and third-party embeds compound the problem

A page embedding widgets from several different third-party domains — a payment form, an analytics snippet, a chat widget — is juggling several independent cross-origin relationships at once, each with its own CORS policy set by a different, external team, which is why a single broken third-party embed can produce a CORS error that has nothing to do with the site's own backend at all, and the first diagnostic step in that situation is confirming which origin the failing request is actually going to before assuming the problem lives anywhere in the team's own infrastructure.

CORS during local development against a real staging API

Pointing a locally running frontend at a real staging or production API, rather than a local backend, is a common setup that immediately becomes cross-origin, and forgetting that staging's CORS allow-list needs to explicitly include whatever origin local development actually runs on — often a specific `localhost` port — is a routine, easily fixed cause of a CORS error that has nothing to do with the application code at all, only with an allow-list that was never updated to include the developer's own machine.