What Is CORS and Why Does It Exist?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which cross-origin requests are allowed. A cross-origin request happens when JavaScript running on https://app.example.com tries to fetch data from https://api.other.com — a different origin (different domain, port, or protocol).
Without CORS, any website could silently read data from your bank, email, or any site you're logged into. CORS exists to prevent exactly this: it lets servers declare which origins they trust.
Importantly, CORS is enforced by the browser, not the server. The server still receives and processes the request — it just includes (or doesn't include) permission headers in the response. The browser then decides whether to expose that response to JavaScript.
The Most Common CORS Error Messages
"Access to XMLHttpRequest blocked by CORS policy: No 'Access-Control-Allow-Origin' header"
The server responded but didn't include the Access-Control-Allow-Origin header. The browser blocks the response from reaching your JavaScript.
Fix: Add the header on your server.
"Access to fetch blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value 'https://other.com' that is not equal to the supplied origin"
The server has CORS enabled, but it's only allowing a different origin than yours.
Fix: Ensure your origin is in the server's allowed origins list.
"CORS preflight response did not succeed"
For non-simple requests (PUT, DELETE, custom headers), the browser sends an OPTIONS preflight first. If that fails, the actual request never fires.
Fix: Handle OPTIONS requests explicitly on your server.
"Request header field Authorization is not allowed by Access-Control-Allow-Headers"
Your request includes a header the server hasn't explicitly allowed.
Fix: Add the header name to Access-Control-Allow-Headers.
What Makes a Request "Simple" vs "Preflighted"?
Simple requests (no preflight):
- Method is GET, HEAD, or POST
- Only uses safe headers:
Accept,Accept-Language,Content-Language,Content-Type(with restrictions) Content-Typeisapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain
Preflighted requests (browser sends OPTIONS first):
- Method is PUT, DELETE, PATCH, or any other method
- Uses custom headers like
Authorization,X-Custom-Header Content-Typeisapplication/json
This means most API calls from modern apps are preflighted because they use Content-Type: application/json or send Authorization headers.
Server-Side Fixes by Framework
Node.js / Express
const express = require('express');
const app = express();
// Option 1: Use the cors package (recommended)
const cors = require('cors');
app.use(cors({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // required if sending cookies/auth headers
maxAge: 86400, // cache preflight for 24 hours
}));
// Option 2: Manual headers
app.use((req, res, next) => {
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(204); // preflight response
}
next();
});
Python / FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com", "https://staging.myapp.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Python / Django
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # must be before CommonMiddleware
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"https://myapp.com",
"https://staging.myapp.com",
]
CORS_ALLOW_CREDENTIALS = True
# Or for development (never in production):
# CORS_ALLOW_ALL_ORIGINS = True
Go / Gin
import "github.com/gin-contrib/cors"
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://myapp.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
Nginx (reverse proxy)
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com' 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 'Access-Control-Allow-Credentials' 'true' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://backend;
}
The credentials Trap
If your request includes cookies or an Authorization header, you must:
- Set
Access-Control-Allow-Credentials: trueon the server - Set
Access-Control-Allow-Originto the specific origin — wildcards (*) don't work with credentials - On the client: set
credentials: 'include'in fetch, orwithCredentials: truein Axios
// fetch
fetch('https://api.example.com/data', {
credentials: 'include', // send cookies
});
// Axios
axios.get('https://api.example.com/data', {
withCredentials: true,
});
If you use Access-Control-Allow-Origin: * together with credentials, the browser will throw: "The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '' when the request's credentials mode is 'include'."*
Development Workarounds (Not for Production)
Vite / webpack dev server proxy
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^/api/, ''),
},
},
},
};
This proxies /api/* requests through Vite's server, making them same-origin to the browser — no CORS headers needed.
Browser extension (testing only)
Extensions like "CORS Unblock" disable CORS enforcement in the browser. Useful for quick testing. Never use in production, never ship code that depends on this.
Security: Never Use Access-Control-Allow-Origin: * on Authenticated APIs
A wildcard allows any origin to read your API response. For public, unauthenticated APIs (CDN assets, public data) this is fine. For any API that uses authentication or returns user-specific data, always specify exact allowed origins.
Debugging CORS Issues
- Open DevTools → Network tab → click the failing request → look at the Response Headers
- Check if
Access-Control-Allow-Originis present and matches your origin - For preflighted requests, find the OPTIONS request that precedes the actual request and check its response
- Use curl to test the raw headers:
curl -I -H "Origin: https://yourapp.com" https://api.example.com/endpoint
→ Use the URL Parser to break down API endpoints and inspect their components.