Understanding CORS: Why It Exists and How to Fix Common Errors
A practical guide to Cross-Origin Resource Sharing (CORS) — why browsers block requests, how preflight works, and step-by-step fixes for every common CORS error.
What is CORS?
Cross-Origin Resource Sharing (CORS) is a security mechanism built into web browsers that controls how web pages can request resources from a different domain (origin) than the one that served the page.
An origin is defined by three parts:
- Protocol (http vs https)
- Domain (example.com)
- Port (80, 443, 3000, etc.)
If any of these differ between the page making the request and the server receiving it, it's a cross-origin request.
Same origin:
https://example.com/page → https://example.com/api ✅
Cross-origin:
https://example.com → https://api.example.com ❌ (different subdomain)
https://example.com → http://example.com ❌ (different protocol)
http://localhost:3000 → http://localhost:8080 ❌ (different port)
Why Does CORS Exist?
Without CORS, any website could make requests to any other website using your browser's cookies and credentials. Imagine visiting a malicious site that silently makes requests to your bank's API using your session cookies — that's exactly what CORS prevents.
The Same-Origin Policy is the default browser security model: scripts can only make requests to the same origin. CORS is the mechanism that allows servers to opt in to cross-origin requests when they want to.
Key insight: CORS is enforced by the browser, not the server. The server sends CORS headers, and the browser decides whether to allow the response. This is why the same request works in Postman or curl but fails in the browser.
How CORS Works
Simple Requests
For "simple" requests (GET, POST with certain content types), the browser sends the request directly and checks the response headers:
Browser → Server:
GET /api/data HTTP/1.1
Origin: https://myapp.com
Server → Browser:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Browser: Origin matches → allow response ✅
A request is "simple" if it meets ALL of these criteria:
- Method is GET, HEAD, or POST
- Only uses headers: Accept, Accept-Language, Content-Language, Content-Type
- Content-Type is: application/x-www-form-urlencoded, multipart/form-data, or text/plain
Preflight Requests
For "non-simple" requests (PUT, DELETE, custom headers, JSON content type), the browser sends a preflight OPTIONS request first:
Step 1 — Preflight:
Browser → Server:
OPTIONS /api/data HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Server → Browser:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Step 2 — Actual request (only if preflight succeeds):
Browser → Server:
PUT /api/data HTTP/1.1
Origin: https://myapp.com
Content-Type: application/json
Authorization: Bearer token123
{"name": "updated"}
Server → Browser:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
CORS Response Headers
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin | Which origins can access | https://myapp.com or * |
Access-Control-Allow-Methods | Allowed HTTP methods | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Allowed request headers | Content-Type, Authorization |
Access-Control-Allow-Credentials | Allow cookies/auth | true |
Access-Control-Expose-Headers | Headers the browser can read | X-Total-Count |
Access-Control-Max-Age | Cache preflight (seconds) | 86400 |
Common CORS Errors and Fixes
Error 1: "No Access-Control-Allow-Origin header"
Access to fetch at 'https://api.example.com/data' from origin
'https://myapp.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
Fix: Add the header on your server:
// Express.js
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "https://myapp.com");
next();
});
// Or use the cors middleware
const cors = require("cors");
app.use(cors({ origin: "https://myapp.com" }));
Error 2: "Wildcard with credentials"
Access to fetch has been blocked: The value of the
'Access-Control-Allow-Origin' header must not be the wildcard '*'
when the request's credentials mode is 'include'.
Fix: You can't use * with credentials: "include". Specify the exact origin:
app.use(cors({
origin: "https://myapp.com", // Not "*"
credentials: true,
}));
Error 3: "Method not allowed"
Method PUT is not allowed by Access-Control-Allow-Methods
Fix: Add the method to allowed methods:
app.use(cors({
origin: "https://myapp.com",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
}));
Error 4: "Header not allowed"
Request header field authorization is not allowed by
Access-Control-Allow-Headers
Fix: Add the header to allowed headers:
app.use(cors({
origin: "https://myapp.com",
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
}));
Error 5: Preflight fails with 404 or 405
Your server doesn't handle OPTIONS requests.
Fix: Handle OPTIONS explicitly or use middleware:
// Express — handle OPTIONS for all routes
app.options("*", cors());
// Or handle it manually
app.options("/api/*", (req, res) => {
res.header("Access-Control-Allow-Origin", "https://myapp.com");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.sendStatus(204);
});
Server Configuration Examples
Express.js
const cors = require("cors");
// Allow all origins (development only!)
app.use(cors());
// Allow specific origin
app.use(cors({
origin: "https://myapp.com",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
}));
// Allow multiple origins
app.use(cors({
origin: ["https://myapp.com", "https://admin.myapp.com"],
}));
// Dynamic origin
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ["https://myapp.com", "https://admin.myapp.com"];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
}));
Next.js API Routes
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "https://myapp.com" },
{ key: "Access-Control-Allow-Methods", value: "GET, POST, PUT, DELETE" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
],
},
];
},
};
Nginx
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-Max-Age 86400 always;
if ($request_method = OPTIONS) {
return 204;
}
}
Development Workarounds
During development, you often need to bypass CORS:
1. Proxy in Development
// Next.js — next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
};
// Vite — vite.config.js
export default {
server: {
proxy: {
"/api": {
target: "https://api.example.com",
changeOrigin: true,
},
},
},
};
2. Browser Extension (Development Only)
Extensions like "CORS Unblock" disable CORS in the browser. Never use in production and never ask users to install one.
Security Best Practices
- Never use
Access-Control-Allow-Origin: *with credentials — It's a security vulnerability - Whitelist specific origins — Don't reflect the Origin header blindly
- Limit allowed methods and headers — Only allow what your API actually needs
- Set
Access-Control-Max-Age— Reduces preflight requests (cache for 24 hours: 86400) - Don't disable CORS in production — Fix the configuration instead
- Validate the Origin header server-side — Don't trust it blindly for security decisions