CORS errors are a rite of passage for web developers. You build something that works perfectly in Postman, then the browser blocks it with a cryptic error about "Access-Control-Allow-Origin." Understanding why CORS exists — not just how to silence the error — makes you much faster at debugging it.
The Same-Origin Policy
Before CORS, there was the same-origin policy. Browsers enforce it to prevent a malicious website from using your credentials to make requests to other sites on your behalf.
Two URLs share the same origin if and only if the scheme, host, and port all match exactly.
https://example.com/page ← origin: https://example.com
https://example.com/other ← same origin ✓
https://api.example.com/data ← different origin (subdomain differs) ✗
http://example.com/page ← different origin (scheme differs) ✗
https://example.com:8080/page ← different origin (port differs) ✗
Without the same-origin policy, if you visited a malicious site while logged into your bank, that site could use fetch() or XMLHttpRequest to make authenticated requests to your bank's API — using your session cookies — and read the response. The same-origin policy blocks cross-origin reads.
What CORS Actually Does
CORS (Cross-Origin Resource Sharing) is a controlled relaxation of the same-origin policy. Servers explicitly declare which other origins they'll accept. The browser enforces the negotiation; the server just provides the permission.
The key headers are:
Access-Control-Allow-Origin— which origins are allowed (a specific origin or*)Access-Control-Allow-Methods— which HTTP methods are permittedAccess-Control-Allow-Headers— which request headers are permittedAccess-Control-Allow-Credentials— whether cookies/auth headers can be sent
None of these headers are set by default. If your server doesn't include them, the browser blocks cross-origin reads.
Simple vs Non-Simple Requests
Not every cross-origin request triggers the same CORS flow. The spec divides requests into "simple" and "non-simple" (also called preflighted).
Simple requests must meet all of these conditions:
- Method is
GET,POST, orHEAD - Only uses "safelisted" headers:
Accept,Accept-Language,Content-Language,Content-Type(with specific values below), andRange Content-Typeis one of:text/plain,multipart/form-data,application/x-www-form-urlencoded
Simple requests are sent immediately, but the browser still blocks the response if the Access-Control-Allow-Origin header doesn't allow the requesting origin.
Non-simple requests — anything with a custom header like Authorization, a JSON body, or PUT/DELETE methods — trigger a preflight. The browser first sends an OPTIONS request to ask what the server allows.
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
The server must respond with the appropriate Access-Control-Allow-* headers. If the preflight succeeds, the browser sends the actual request.
Common CORS Errors and Their Fixes
"No 'Access-Control-Allow-Origin' header is present"
The server isn't sending any CORS headers. Fix: add the header to your server's response.
// Express.js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://yourfrontend.com');
next();
});
// Or use the cors package:
const cors = require('cors');
app.use(cors({ origin: 'https://yourfrontend.com' }));
"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials are included"
You're using credentials: 'include' in your fetch call but the server returns Access-Control-Allow-Origin: *. Wildcards and credentials cannot be combined.
Fix: specify the exact origin and also set Access-Control-Allow-Credentials: true.
app.use(cors({
origin: 'https://yourfrontend.com',
credentials: true,
}));
"Method PUT is not allowed by Access-Control-Allow-Methods"
The preflight response doesn't list the method you're using. Fix: add it to the allowed methods.
app.use(cors({
origin: 'https://yourfrontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
}));
"Request header 'Authorization' is not allowed by Access-Control-Allow-Headers"
Same problem, but for headers. Fix: include the header in Access-Control-Allow-Headers.
app.use(cors({
origin: 'https://yourfrontend.com',
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Preflight OPTIONS request returning 404 or 405
Some frameworks or routers don't handle OPTIONS requests by default. You need an explicit handler, or ensure your CORS middleware runs before routing.
// Express: handle preflight explicitly
app.options('*', cors(corsOptions));
app.use(cors(corsOptions));
Credentials and withCredentials
By default, cross-origin fetch calls don't include cookies or HTTP authentication. You have to opt in explicitly.
// Fetch API
fetch('https://api.example.com/data', {
credentials: 'include', // sends cookies
});
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
This only works when the server responds with Access-Control-Allow-Credentials: true and specifies an exact origin (not *). The browser enforces both conditions.
Why You Shouldn't Use Wildcard with Credentials
Access-Control-Allow-Origin: * means any website can read your API's responses. For truly public APIs (weather data, public datasets), that's fine. For APIs that handle authenticated sessions, it means any site can access data on behalf of your logged-in users.
Setting * with credentials isn't just bad practice — the browser actively refuses it. The spec forbids the combination because it would completely defeat the same-origin policy.
If you need to allow multiple origins (development + production), maintain an allowlist on the server side:
const allowedOrigins = [
'https://yourapp.com',
'https://www.yourapp.com',
'http://localhost:3000',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
A Few Things CORS Doesn't Do
CORS is a browser mechanism. Server-to-server requests aren't affected — when your Node.js backend calls another API, there's no CORS check. The browser is the enforcing party.
It also doesn't protect against all cross-origin attacks. Cross-Site Request Forgery (CSRF) involves the browser sending authenticated requests to your API, but the attacker's page doesn't need to read the response. CORS blocks the read; it doesn't block the write. You still need CSRF tokens or SameSite cookie attributes for that.
If you're debugging a CORS issue, look at your JSON Formatter to inspect the actual JSON response bodies once headers are sorted out. And if you need to inspect what the server is actually returning — check the Network tab in DevTools, not just the console error, which often strips out useful detail.
The Developer Workflow
When you hit a CORS error, check these in order:
- Is the
Access-Control-Allow-Originheader present at all? If not, your CORS middleware isn't running or isn't matching the route. - Does the value match the requesting origin exactly? (Protocol, host, and port all matter.)
- Is it a preflight? Check for an
OPTIONSrequest in the Network tab. Did it return 200 with the rightAccess-Control-Allow-*headers? - Are you sending credentials? Make sure the server responds with
Access-Control-Allow-Credentials: trueand a specific origin. - Is the blocked header listed in
Access-Control-Allow-Headers?
For understanding the full picture of HTTP headers including CORS-related ones, see Understanding HTTP Headers. For URL encoding in cross-origin request parameters, the URL Encoder is useful when you need to pass data safely in query strings across origins.