CORS (Cross-Origin Resource Sharing) is responsible for more confused developer Stack Overflow questions than almost any other browser feature. The error messages are cryptic, the behavior is counterintuitive, and the fix is almost always on the server — not the browser.
This guide covers how CORS actually works, why you're seeing these errors, and how to configure it correctly.
What Is a Cross-Origin Request?
Two URLs share the same "origin" if they have the same protocol, host, and port:
https://app.example.com <- origin A
https://api.example.com <- different origin (different subdomain)
http://app.example.com <- different origin (different protocol)
https://app.example.com:8080 <- different origin (different port)
When JavaScript in a browser makes a fetch/XHR to a different origin, the browser enforces CORS rules. This is a browser-side security feature — the request may well succeed on the server, but the browser blocks the JavaScript from reading the response.
The Two Types of CORS Requests
Simple Requests
A "simple request" goes through in one round trip. Criteria:
- Method is GET, POST, or HEAD
- Content-Type is
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No custom headers
The browser sends the request and checks Access-Control-Allow-Origin in the response:
GET /api/data HTTP/1.1
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
If the header matches (or is *), the browser lets JavaScript read the response.
Preflighted Requests
For anything more complex — PUT/DELETE, Content-Type: application/json, custom headers — the browser first sends an OPTIONS preflight:
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 to the preflight before the actual request proceeds:
HTTP/1.1 204 No Content
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-Max-Age: 86400
If the preflight response is missing or wrong, the actual request never fires.
Common CORS Error Messages
"No 'Access-Control-Allow-Origin' header"
The server didn't send the CORS header at all. The request succeeded server-side — you just can't read it from JavaScript.
Fix: Add Access-Control-Allow-Origin to your server response.
"value ... must not be the wildcard '*' when the request's credentials mode is 'include'"
You're sending cookies/auth with credentials: 'include', but the server responds with Access-Control-Allow-Origin: *.
Fix: Set a specific origin AND add Access-Control-Allow-Credentials: true.
// Wrong:
res.setHeader('Access-Control-Allow-Origin', '*');
// Right (when using credentials):
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
"Method DELETE is not allowed by Access-Control-Allow-Methods"
Your preflight doesn't include the method you're using. Add it to Access-Control-Allow-Methods.
"Request header field Authorization is not allowed by Access-Control-Request-Headers"
Add Authorization to your Access-Control-Allow-Headers preflight response.
Server-Side Configuration
Express (Node.js)
const cors = require('cors');
// Public API — any origin, no credentials
app.use(cors());
// Authenticated API — specific origins, credentials
app.use(cors({
origin: ['https://app.example.com', 'https://staging.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));
// Dynamic origin validation (e.g. wildcard subdomain)
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com'];
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Nginx
location /api {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
proxy_pass http://backend;
}
The always flag is critical — without it, Nginx only adds CORS headers on 200 responses, not error responses. You'll get CORS errors on 500s and 404s.
Django
# pip install django-cors-headers
# settings.py
INSTALLED_APPS = ['corsheaders', ...]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = ['https://app.example.com']
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = ['accept', 'authorization', 'content-type']
Credentials and Cookies
Client side:
fetch('https://api.example.com/data', {
credentials: 'include' // sends cookies
});
// axios
axios.get('https://api.example.com/data', { withCredentials: true });
Both conditions required on the server:
-
Access-Control-Allow-Origin= specific origin (never*) Access-Control-Allow-Credentials: true
Preflight Caching
Every preflight adds a round trip. Cache them with Access-Control-Max-Age:
Access-Control-Max-Age: 86400 # 1 day
Firefox caches up to 24 hours; Chrome up to 2 hours. Set to 86400 and let browsers apply their caps.
Development: Use a Proxy Instead
Avoid CORS entirely in development by proxying API calls through your dev server:
Vite:
export default {
server: {
proxy: {
'/api': { target: 'http://localhost:3001', changeOrigin: true }
}
}
}
Create React App:
{ "proxy": "http://localhost:3001" }
Same origin — no CORS required.
Debugging CORS in Firefox DevTools
Firefox gives the most detailed CORS error messages:
- Open Network tab
- Click the failed request
- Firefox often shows the exact missing header in the response section
- Preflight (
OPTIONS) requests appear explicitly, labeled separately
The Security tab on a request also shows which CORS policy blocked access.
Common Gotchas
Wildcard subdomain matching doesn't exist. *.example.com is not valid in Access-Control-Allow-Origin. Dynamically validate and echo the specific origin:
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && /^https:\/\/[\w-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
next();
});
CORS errors on 4xx/5xx responses: If your middleware only adds CORS headers on success, preflight failures return CORS errors instead of the actual error. Use always in Nginx, or ensure your error handlers also set the headers.
null origin: Locally-opened HTML files send Origin: null. Never add null to your allowed origins — any sandboxed iframe can exploit this.
CORS is not an API security feature. Server-to-server requests and curl bypass CORS entirely. It's a browser-enforced policy for JavaScript — you still need authentication for your API.
Understanding CORS becomes straightforward once you internalize that the browser is the enforcer, not the server, and the fix is always a header the server needs to send.
Top comments (0)