The first time you see a CORS error it feels like the request is being blocked by the server. It isn't. CORS errors happen in the browser, as an enforcement of a security policy the browser runs before handing a cross-origin response to your JavaScript. But the fix lives on the server — which is why the error is so disorienting when you first meet it.
The same-origin policy: the rule CORS extends
Browsers implement a same-origin policy: by default, JavaScript running on https://app.example.com can only read the response of a request to https://app.example.com — the same scheme, host, and port. A request to https://api.example.com (different subdomain), http://app.example.com (different scheme), or https://app.example.com:8080 (different port) is a cross-origin request and triggers a different set of rules.
Note: sending the request is usually allowed; reading the response is what the same-origin policy controls. This distinction matters for understanding what CORS actually does.
What CORS does (and doesn't) protect against
CORS is not about hiding your data from the public internet. Anyone can curl an API endpoint directly and get the response regardless of any CORS headers. CORS specifically restricts which web pages (running in which browsers) can read a cross-origin response in JavaScript.
The underlying threat it mitigates is cross-site request forgery of authenticated requests: without CORS, a malicious page on evil.com could make a credentialed fetch() to your banking app and read the response using your session cookies. CORS lets the banking server say "I only give readable responses to JavaScript loaded from app.mybank.com."
Simple vs preflighted requests
Not all cross-origin requests are treated the same. A simple request — GET or POST with only a handful of allowed headers and a text/plain or multipart/form-data body — goes straight through; the browser checks the response headers before passing the response to JavaScript.
A preflighted request is any request that doesn't meet the simple criteria: PUT, DELETE, PATCH, a JSON body with Content-Type: application/json, or custom headers like Authorization. For these, the browser first sends an OPTIONS request — the preflight — asking the server: "would you allow this actual request from this origin?" Only if the server responds permissively does the browser send the real request.
This is why adding Authorization to a fetch that previously worked can suddenly trigger a CORS error: the header pushed it from simple to preflighted, and the server's CORS configuration either doesn't handle OPTIONS or doesn't include Authorization in its Access-Control-Allow-Headers.
The headers that matter
Response headers the server must send:
| Header | What it does |
| --- | --- |
| Access-Control-Allow-Origin | The origin(s) allowed to read the response. Use * for public APIs, specific origin for credentialed requests. |
| Access-Control-Allow-Methods | Comma-separated list of HTTP methods permitted (GET, POST, PUT, DELETE). |
| Access-Control-Allow-Headers | Headers the client is allowed to include (Content-Type, Authorization). |
| Access-Control-Allow-Credentials | true if cookies and auth headers should be included. Cannot be combined with Allow-Origin: *. |
| Access-Control-Max-Age | How long (seconds) the browser can cache the preflight result. Reduces OPTIONS round-trips. |
One sharp edge: Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true cannot coexist. Browsers reject that combination because it would let any page on the internet make credentialed requests on a user's behalf. For credentialed CORS, you must echo back the specific requesting origin — which means your server needs to inspect the Origin header and reflect it, or maintain an allowlist.
Debugging CORS in practice
- Open the Network tab, find the failed request, and look at the response headers. If
Access-Control-Allow-Originis absent or wrong, that's your problem. - Look for the OPTIONS preflight — it appears just before the failing request. A 404 or 405 on the OPTIONS request means your server isn't handling preflight at all.
- Check the console error message closely — browsers have gotten specific: "CORS header 'Access-Control-Allow-Origin' does not match…" or "credential flag is 'true', but the 'Access-Control-Allow-Credentials' header is…" tells you exactly which header is wrong.
- Don't disable the same-origin policy in your browser as a "fix" — it only masks the problem on your machine and ships broken code to users.
Build the headers once, get them right
Our CORS Headers Builder assembles the full set of Access-Control-* response headers from structured inputs — origin allowlist, methods, headers, credentials, preflight TTL — so you can copy them directly into your server config, Express middleware, or CDN rules without memorizing the exact header names and the */credential incompatibility.
