Pavan Rangani

HomeBlogCORS, Explained by What It Actually Blocks

CORS, Explained by What It Actually Blocks

By Pavan Rangani · August 18, 2026 · Security

No web error is more misunderstood than a CORS error, and the confusion is understandable, because almost everything about it is counterintuitive. It is enforced by the browser, not your server. It protects the user, not your API. And the instinct it triggers — “just turn it off” — is usually exactly the wrong fix. Understanding CORS by what it actually blocks, and why, turns a maddening error into a five-minute fix.

What CORS is actually for

CORS — Cross-Origin Resource Sharing — exists because of a rule the browser enforces called the same-origin policy. By default, JavaScript running on one origin (a scheme, host, and port combination like https://app.example.com) cannot read responses from a different origin. CORS is the mechanism by which a server can relax that rule and say “it’s fine for this other origin to read my responses.”

The reason this matters for security is subtle and worth getting right. Without the same-origin policy, a malicious site you visit could quietly make requests to your bank — using the cookies your browser automatically attaches — and read the responses, scraping your account details. The same-origin policy stops the malicious site from reading those responses. CORS is not there to protect your API from attackers; it is there to protect your users from other sites acting on their behalf. That reframing explains most of the otherwise-baffling behavior.

Why it is the browser, not your server

The single most confusing thing about CORS is where it is enforced. When you get a CORS error, your server very likely received the request and sent a response perfectly fine. It is the browser that then refuses to let your JavaScript read that response, because the response did not include the header saying this origin is allowed. The block happens on the client side, after the round trip, which is why your server logs show a successful request while your console shows a failure.

This has two immediate consequences. First, tools that are not browsers — curl, Postman, your backend calling another backend — are not subject to CORS at all, which is why “it works in Postman but not the browser” is the signature symptom. Second, you cannot fix a CORS error by changing your frontend code, because the frontend is not the thing enforcing it; the fix is always on the server that owns the resource, by adding the right headers. Developers lose hours trying to configure their way around it on the client, and it simply cannot be done from there.

The preflight request

For any request beyond the simplest kinds, the browser does something that surprises people: before sending your actual request, it sends a separate OPTIONS request first, called a preflight, asking the server “am I allowed to make this request?” Only if the server answers with the right permissions does the browser send the real request. So one fetch in your code can become two requests on the wire.

This explains a whole class of confusing symptoms. A GET might work while a POST with a JSON body fails, because the JSON content type triggers a preflight that the server is not answering correctly. Custom headers like an Authorization token trigger a preflight too. When your request mysteriously fails and you see an OPTIONS request in the network tab that you never wrote, the preflight is being rejected — the server needs to handle that OPTIONS request and respond with which methods and headers it permits, not just handle the POST itself.

Fixing it correctly

The right fix is to configure the server that owns the resource to send the appropriate Access-Control-Allow-Origin header, naming the specific origins allowed to read its responses. For a request that sends cookies or credentials, the server must also send Access-Control-Allow-Credentials: true — and, importantly, cannot use the wildcard origin in that case, because the browser refuses to send credentials to a server that allows “any origin.” You must name the exact origin.

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true   // then Allow-Origin cannot be *

And the server must handle the preflight OPTIONS request, responding with these headers, or the real request never gets sent. Most web frameworks have a CORS middleware that does all of this once you configure the allowed origins — reach for that rather than setting headers by hand, since the interactions between origin, credentials, methods, and preflight are exactly the kind of detail a library gets right and hand-rolled code gets subtly wrong.

The mistake that undoes the point

The dangerous “fix” is setting Access-Control-Allow-Origin: * to make the error go away. This tells the browser that any website is allowed to read your API’s responses, which throws away the protection CORS provides. For a genuinely public API serving only public data with no credentials, a wildcard can be acceptable. But for anything that returns user-specific data or accepts authenticated requests, a wildcard means any malicious site can read your users’ data through their browsers — the exact attack the same-origin policy was preventing.

So the correct instinct when you hit a CORS error is not “how do I disable this,” but “which specific origins should legitimately be allowed to read this, and does this request carry credentials?” Answer those two questions, name the real origins, handle the preflight, and the error disappears without opening a hole. CORS is not an obstacle between you and a working app; it is one of several browser security protections working as designed, and configuring it correctly takes about as long as disabling it dangerously.

← Back to all articles