Access-Control-Allow-Origin (and related) headers to the server's
response.Access-Control-Allow-Origin. Missing or wrong? That's your fix target.Access-Control-Allow-Origin: * cannot be combined with
Access-Control-Allow-Credentials: true. Echo the specific origin instead. Also, the
Authorization header is never covered by the * wildcard in
Access-Control-Allow-Headers — it must be listed explicitly.Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true is
rejected by all browsers. Echo the specific requesting origin and add Vary: Origin. [src1, src4]Access-Control-Allow-Origin to a fetch() request header does nothing — the
browser sets Origin automatically and checks response headers. [src1, src2]always in nginx or ensure middleware runs unconditionally. [src3, src4]Access-Control-Allow-Private-Network: true. Without this,
Chrome blocks the request entirely. [src7]* wildcard in
Access-Control-Allow-Headers does not cover Authorization — you must list it
explicitly. [src1, src6]| # | Error / Scenario | Root Cause | Fix |
|---|---|---|---|
| 1 | No 'Access-Control-Allow-Origin' header | Server doesn't send the header | Add Access-Control-Allow-Origin to server response [src1, src3] |
| 2 | Origin not allowed | Origin not in server's allowlist | Add specific origin to allowlist [src1, src4] |
| 3 | Preflight (OPTIONS) fails | Server doesn't handle OPTIONS | Return CORS headers on OPTIONS with 204 [src1, src6] |
| 4 | Wildcard + credentials | * can't be used with credentials |
Echo specific origin; set Vary: Origin [src1, src4] |
| 5 | Custom header blocked | Header not in Allow-Headers |
Add to Access-Control-Allow-Headers [src1, src3] |
| 6 | Method blocked | Method not in Allow-Methods |
Add to Access-Control-Allow-Methods [src1] |
| 7 | Cookie not sent cross-origin | credentials: 'include' missing |
Add on client + Allow-Credentials: true on server [src1, src4] |
| 8 | Trailing slash mismatch | https://example.com/ ≠ https://example.com |
Strip trailing slash from origin comparison [src3, src5] |
| 9 | HTTP vs HTTPS mismatch | Protocol is part of origin | Ensure both sides use same protocol [src2, src3] |
| 10 | Port mismatch | Port is part of origin | Include port in allowed origins [src2, src3] |
| 11 | Redirect changes origin | Redirect to different origin | Handle CORS on redirect target too [src1, src6] |
| 12 | Private Network Access blocked | Public site accessing private IP | Add Access-Control-Allow-Private-Network: true to preflight response [src7]
|
| 13 | Authorization header not allowed | * wildcard doesn't cover Authorization |
Explicitly list Authorization in Access-Control-Allow-Headers [src1, src6] |
START — Browser shows CORS error
├── DevTools → Network → find the failed request
│ ├── Preflight OPTIONS request present?
│ │ ├── YES — OPTIONS fails (4xx or no CORS headers)
│ │ │ ├── 404/405 on OPTIONS → add OPTIONS handler [src1, src6]
│ │ │ ├── "No Access-Control-Allow-Private-Network" → add PNA header [src7]
│ │ │ └── OPTIONS ok but actual fails → check Allow-Methods/Headers [src1]
│ │ └── NO — Simple request
│ │ ├── No Access-Control-Allow-Origin → add to server [src1, src3]
│ │ ├── Wrong origin → fix allowlist [src1, src4]
│ │ └── Has * but credentials → echo specific origin [src1, src4]
├── Request includes cookies / Authorization?
│ ├── YES → specific origin (not *) + Allow-Credentials: true
│ │ + explicit Authorization in Allow-Headers [src1, src4, src6]
│ └── NO → Wildcard * is fine for public APIs [src1, src5]
├── Custom header blocked (Authorization, X-API-Key)?
│ └── Add to Access-Control-Allow-Headers (Authorization always explicit) [src1, src3]
├── Public site accessing localhost / private IP?
│ └── Add Access-Control-Allow-Private-Network: true [src7]
└── Development only? → Use dev proxy (Vite/webpack) [src3, src5]
Read the browser console — the message tells you exactly what's missing. [src1, src3]
# Common messages:
"No 'Access-Control-Allow-Origin' header is present"
→ Server doesn't send the header at all
"The 'Access-Control-Allow-Origin' header has a value that is not equal to the supplied origin"
→ Server sends wrong origin
"Response to preflight request doesn't pass access control check"
→ Server doesn't handle OPTIONS preflight
"The value of 'Access-Control-Allow-Origin' must not be '*' when credentials mode is 'include'"
→ Wildcard + credentials conflict
"No 'Access-Control-Allow-Private-Network' header was present in the preflight response"
→ Private Network Access preflight missing [src7]
The cors npm package is the standard solution. [src1, src3, src4]
const cors = require('cors');
const allowedOrigins = ['https://app.example.com', 'https://www.example.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
callback(new Error(`CORS: origin ${origin} not allowed`));
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
credentials: true,
maxAge: 86400,
}));
app.options('*', cors()); // Handle preflight for all routes
Configure CORS headers in nginx for any backend. [src1, src3]
location /api/ {
set $cors_origin "";
if ($http_origin ~* "^https://(app|www)\.example\.com$") {
set $cors_origin $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors_origin 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;
add_header 'Vary' 'Origin' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' '86400';
add_header 'Content-Length' '0';
return 204;
}
proxy_pass http://backend:8080;
}
Standard CORS middleware for Python frameworks. [src1, src4]
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400)
# Flask
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"],
supports_credentials=True,
allow_headers=["Content-Type", "Authorization"])
Never disable browser security. Use a dev proxy instead. [src3, src5]
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
Credentials must be explicitly requested on the client. [src1, src4]
// Include cookies and auth headers cross-origin
fetch('https://api.example.com/data', {
credentials: 'include',
headers: { 'Authorization': 'Bearer token123' },
});
// axios
axios.get('https://api.example.com/data', { withCredentials: true });
Public websites accessing localhost/private IPs need PNA headers. [src7]
// Express middleware for Private Network Access
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.headers['access-control-request-private-network'] === 'true') {
res.header('Access-Control-Allow-Private-Network', 'true');
}
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
const express = require('express');
const cors = require('cors');
const app = express();
const allowedOrigins = [
'https://app.example.com',
'https://www.example.com',
];
app.use(cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origin ${origin} not allowed`));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key'],
credentials: true,
maxAge: 86400,
}));
app.options('*', cors());
app.get('/api/data', (req, res) => {
res.json({ data: 'hello' });
});
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
set $cors_origin "";
if ($http_origin ~* "^https://(app|www)\.example\.com$") {
set $cors_origin $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' '86400' always;
add_header 'Vary' 'Origin' always;
if ($request_method = 'OPTIONS') {
add_header 'Content-Length' '0';
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
proxy_pass http://backend:8080;
}
}
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"https://www.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-API-Key"],
max_age=86400,
)
@app.get("/api/data")
async def get_data():
return {"data": "hello"}
// For local dev servers accessed from public websites (Chrome 130+)
const express = require('express');
const app = express();
app.use((req, res, next) => {
const origin = req.headers.origin;
res.header('Access-Control-Allow-Origin', origin || '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle Private Network Access preflight
if (req.headers['access-control-request-private-network'] === 'true') {
res.header('Access-Control-Allow-Private-Network', 'true');
}
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// GOOD — reflect the specific requesting origin [src1, src4]
app.use(cors({
origin: (origin, cb) => cb(null, allowedOrigins.includes(origin)),
credentials: true,
}));
// Response: Access-Control-Allow-Origin: https://app.example.com
// Access-Control-Allow-Credentials: true
// Vary: Origin
# BAD — dangerous, masks real issues [src3, src5]
chrome.exe --disable-web-security --user-data-dir="%TEMP%\chrome_dev"
// GOOD — vite.config.js [src3, src5]
export default {
server: {
proxy: {
'/api': { target: 'https://api.example.com', changeOrigin: true }
}
}
};
// BAD — CORS headers are response headers; client can't set them [src1, src2]
fetch(url, {
headers: {
'Access-Control-Allow-Origin': '*', // Does nothing!
}
});
// GOOD — CORS is always fixed on the server [src1, src2]
// In Express: app.use(cors({ origin: 'https://myapp.com' }))
// In nginx: add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
// BAD — gives opaque response you can't read [src1, src6]
const response = await fetch('https://api.example.com/data', {
mode: 'no-cors', // Silences error but response.json() will fail
});
// response.type === 'opaque', status === 0, body unreadable
// GOOD — fix the server, or proxy through your own backend [src1, src3]
// Option 1: Fix server CORS headers
// Option 2: Proxy through your own backend
const response = await fetch('/api/proxy/data'); // Same-origin, no CORS needed
Vary: Origin header: If your server echoes the requesting origin
dynamically, add Vary: Origin to prevent CDNs from caching a response for one origin and
serving it to another. [src1, src6]Access-Control-Max-Age caches preflight results. Set to
0 during development; 86400 in production. Chrome caps this at 7200 (2h). [src1, src6]always in nginx or ensure middleware
runs before error handlers. [src3, src4]https://example.com ≠
https://example.com/ ≠ https://example.com:443. Origin comparison is exact.
[src2, src3]SameSite=None; Secure: For cross-origin cookies to be sent
(Chrome 80+), they must have these attributes. Without them, credentials: 'include' sends
no cookies. [src1, src4]Access-Control-Request-Private-Network, requests silently fail. [src7]Access-Control-Allow-Headers: * does not cover Authorization per the Fetch
spec. Always list it explicitly. [src1, src6]# Test CORS headers with curl (simple request)
curl -H "Origin: https://app.example.com" \
-v https://api.example.com/data 2>&1 | grep -i "access-control"
# Test preflight request
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v https://api.example.com/data 2>&1 | grep -i "access-control"
# Test Private Network Access preflight (Chrome 130+)
curl -X OPTIONS \
-H "Origin: https://public-site.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Private-Network: true" \
-v http://localhost:3000/api 2>&1 | grep -i "access-control"
# Check all response headers
curl -I -H "Origin: https://app.example.com" https://api.example.com/data
# Browser DevTools steps:
# 1. F12 → Network tab
# 2. Find failed request (red)
# 3. Click → Headers → Response Headers
# 4. Look for: Access-Control-Allow-Origin, Access-Control-Allow-Credentials
# 5. Find OPTIONS preflight above the main request (if any)
Express (Node.js): npm install cors → app.use(cors({ origin: '...', credentials: true }))
FastAPI (Python): CORSMiddleware(allow_origins=[...], allow_credentials=True)
Flask (Python): pip install flask-cors → CORS(app, origins=[...], supports_credentials=True)
Django (Python): pip install django-cors-headers → CORS_ALLOWED_ORIGINS = [...]
Spring Boot (Java): @CrossOrigin(origins = "...") or global CorsConfigurationSource bean
ASP.NET Core (C#): builder.Services.AddCors(...) → app.UseCors("MyPolicy")
nginx: add_header 'Access-Control-Allow-Origin' '...' always; add_header 'Vary' 'Origin' always;
Apache: Header always set Access-Control-Allow-Origin "..."
| Version / Standard | Behavior | Notes |
|---|---|---|
| Fetch Living Standard (current) | Full CORS | credentials: 'include' replaces XHR withCredentials; wildcard
* for Allow-Headers does not cover Authorization [src6] |
| Chrome 130+ (2024-10) | Private Network Access enforced | Public-to-private requests require
Access-Control-Allow-Private-Network: true preflight [src7] |
| Chrome 80+ (2020) | SameSite=None required | Cross-origin cookies need SameSite=None; Secure [src1] |
| Chrome 66+ (2018) | Preflight caching limited | Access-Control-Max-Age capped at 7200s (2h) in Chrome [src1] |
| Safari 12+ (2018) | Full CORS | ITP may block third-party cookies even with CORS [src1] |
| Firefox 3.5+ (2009) | Full CORS | First browser to implement CORS [src1] |
| IE 10+ (2012) | XDomainRequest deprecated | Use standard Fetch/XHR [src1] |
| RFC 6454 (2011) | Origin header | Defines the Origin header format [src2] |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Browser JS calls a different domain/port/protocol | Server-to-server calls | No CORS needed for server-to-server |
| API needs cross-origin browser access | Same-origin requests | No CORS config needed |
| Need cookies/auth cross-origin | Using CDN for static assets | CDN doesn't need CORS for HTML/CSS/JS (but does for Fetch/XHR) |
| Embedding resources from another domain | Mobile apps (React Native) | Mobile apps don't enforce CORS |
| Public site accessing localhost/private IPs | WebSocket connections | WebSockets don't use CORS (different security model) |
Access-Control-Allow-Origin: * is safe for truly public APIs (open datasets, public weather
data) but must never be used with credentials: true or for user-specific data. [src1, src8]Access-Control-Max-Age
to cache preflight results and reduce OPTIONS round-trips. [src1, src6]always in nginx.
[src3, src4]Origin header but have their own
security model. [src1]fetch() mode 'no-cors' is a trap: It silences the CORS error
but gives you an opaque response you can't read. Only useful for fire-and-forget requests. [src1, src6]