CORS Errors Explained: Every Fix, Every Framework (2026 Guide)
TL;DR — A CORS error means the browser blocked a cross-origin request because the server did not explicitly allow it. The fix is always server-side: return the correct Access-Control-Allow-Origin header from your backend. This guide covers every CORS error type, a step-by-step diagnosis flow, and copy-paste fixes for Express, FastAPI, Next.js, nginx, Cloudflare Workers, and Vercel. You can inspect and validate your CORS headers live with the CORS Header Checker — no curl, no Postman, no install.
What CORS Actually Is (and Why the Browser Enforces It)
The Same-Origin Policy (SOP) is a browser security rule: JavaScript running on https://myapp.com can only read responses from requests made to the same origin — same scheme, same host, same port. Everything else is cross-origin.
CORS — Cross-Origin Resource Sharing — is the mechanism that lets servers selectively relax the Same-Origin Policy. A server adds HTTP headers to its responses that tell the browser: "it is okay to share this response with code from origin X." Without those headers, the browser reads the response, then silently discards it and throws a CORS error into your console.
Three things to burn into memory before you read further:
-
CORS is enforced by the browser, not the server.
curland Postman do not check CORS — they always get the response. Only browsers do CORS. If your API works in Postman but fails in the browser, CORS is almost certainly why. - The fix is server-side, always. Browser extensions that "disable CORS" are masking the problem in your local browser only. They break for every real user. Never ship code that depends on them.
-
Preflight is a separate request. For non-simple requests (anything with a custom header, a JSON body, or methods other than GET/POST), the browser sends an
OPTIONSrequest first to ask for permission. Your server must handle this correctly.
The Four CORS Error Types — Diagnosed from the Console Message
Error 1: "No 'Access-Control-Allow-Origin' header is present"
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.
Cause: The server returned a response with no CORS headers at all. This is the most common error. Your API is running and returning data, but it never told the browser it was okay to share that data.
Fix: Add Access-Control-Allow-Origin to your server's response headers. See the framework-specific fixes below.
Error 2: "The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'"
Access to fetch at 'https://api.example.com' from origin 'https://myapp.com'
has been blocked by CORS policy: The value of the
'Access-Control-Allow-Origin' header in the response must not be the
wildcard '*' when the request's credentials mode is 'include'.
Cause: You set Access-Control-Allow-Origin: * but your frontend sends credentials (cookies, Authorization headers, or TLS client certificates) using fetch with credentials: 'include'. These two settings are mutually exclusive — the wildcard is forbidden when credentials are involved.
Fix: Change * to the exact origin of your frontend. You must also set Access-Control-Allow-Credentials: true.
// ❌ Wrong — won't work with credentials
res.setHeader('Access-Control-Allow-Origin', '*');
// ✅ Correct
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Error 3: "Request header field X is not allowed by Access-Control-Allow-Headers in preflight response"
Access to fetch at 'https://api.example.com' has been blocked by CORS policy:
Request header field Authorization is not allowed by
Access-Control-Allow-Headers in preflight response.
Cause: Your frontend includes a custom header (Authorization, Content-Type: application/json, X-Custom-Header, etc.) that your server does not explicitly allow in its preflight response.
Fix: Add the missing header to Access-Control-Allow-Headers in your preflight OPTIONS response:
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Error 4: "Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response"
Cause: Your frontend uses PUT, DELETE, PATCH, or another non-simple method that your server has not explicitly allowed.
Fix: Add the method to Access-Control-Allow-Methods:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Step-by-Step CORS Diagnosis Flow
Before diving into framework fixes, diagnose which layer the problem lives in:
Step 1 — Confirm it is actually a CORS error. Open DevTools → Network tab → find the failed request. Look at the Response headers tab. Is Access-Control-Allow-Origin missing? That is Error 1. Is the value * but you send cookies? That is Error 2.
Step 2 — Check if the server is returning anything at all. If the response status is 0 or the request shows "failed" in red with no status code, the request may be getting blocked before it reaches your server (firewall, missing DNS, TLS error). CORS errors show a status code but the body is blocked.
Step 3 — Inspect the preflight. Look for an OPTIONS request to the same URL in the Network tab. If it is missing when you expect it, your browser decided the request is "simple" and skipped it. If it exists but returns a non-2xx status, your server is rejecting OPTIONS at the routing level before CORS headers are set.
Step 4 — Test with the CORS checker. Paste your API endpoint into the AllDevToolsHub CORS Header Checker to see exactly which headers your server returns, without writing a single line of code.
Fixes by Framework
Express.js (Node.js)
Install the cors package:
npm install cors
Simple — allow one origin:
const cors = require('cors');
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // only if you send cookies/auth headers
}));
Dynamic — allow multiple origins from a list:
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blocked: ${origin}`));
}
},
credentials: true,
}));
Handle preflight explicitly (necessary if your route-level middleware conflicts):
app.options('*', cors()); // Enable pre-flight across all routes
FastAPI (Python)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"], # use ["*"] only for fully public APIs
allow_credentials=True, # set False if using allow_origins=["*"]
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
)
Next.js (App Router)
Add headers in next.config.ts:
// next.config.ts
const nextConfig = {
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,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization' },
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
],
},
];
},
};
export default nextConfig;
Or handle it inside a Route Handler for per-route control:
// app/api/data/route.ts
export async function OPTIONS(request: Request) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
export async function GET(request: Request) {
return Response.json({ data: 'hello' }, {
headers: {
'Access-Control-Allow-Origin': 'https://myapp.com',
},
});
}
nginx
server {
listen 443 ssl;
server_name api.example.com;
location / {
# Handle preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://localhost:3000;
}
}
⚠️ nginx's
add_headeronly applies when the status code is2xxor3xxby default. Useadd_header ... always;to include headers on error responses too.
Cloudflare Workers
const ALLOWED_ORIGIN = 'https://myapp.com';
export default {
async fetch(request) {
const origin = request.headers.get('Origin');
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': origin === ALLOWED_ORIGIN ? origin : '',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
},
});
}
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('Access-Control-Allow-Origin',
origin === ALLOWED_ORIGIN ? origin : '');
return newResponse;
},
};
Vercel (vercel.json)
{
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "https://myapp.com" },
{ "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE,OPTIONS" },
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type,Authorization" },
{ "key": "Access-Control-Allow-Credentials", "value": "true" }
]
}
]
}
The Access-Control-Max-Age Performance Header
Every CORS preflight is an extra OPTIONS request that adds latency before your actual request fires. You can cache the preflight result with Access-Control-Max-Age:
Access-Control-Max-Age: 86400
This tells the browser: "you do not need to send another OPTIONS for 24 hours." Chrome respects a maximum of 7,200 seconds (2 hours); Firefox respects up to 86,400. Set it as high as your browser allows. This is a free performance win that most guides skip.
Private Network Access — The 2026 Gotcha
Since Chrome 98, there is an additional CORS-adjacent policy called Private Network Access (CORS-RFC1918). If a public website (HTTPS) tries to fetch a resource from a private network address (localhost, 192.168.x.x, 10.x.x.x, or a .local hostname), Chrome sends a preflight with an extra header:
Access-Control-Request-Private-Network: true
Your local server must respond with:
Access-Control-Allow-Private-Network: true
If your local dev server does not return this header, Chrome blocks the request even if your normal CORS headers are correct. This catches a lot of developers by surprise when their production frontend cannot talk to a local development backend.
Firefox is implementing the same spec; it will apply broadly by late 2026.
Fix for local dev servers:
-
vite: addserver.cors: trueor a custom middleware -
webpack-dev-server: addheaders: { 'Access-Control-Allow-Private-Network': 'true' }todevServer.headers - Express local: add the header manually in your CORS middleware
The * Wildcard — When It Is Safe and When It Is Dangerous
Access-Control-Allow-Origin: * is fine for:
- Completely public, read-only APIs (a weather API, a public data endpoint)
- CDN-hosted static assets
- Font files
* is never safe for:
- Any API that reads or mutates user data
- Any endpoint the client accesses with cookies or an
Authorizationheader - Any internal service
The danger: if you return *, any website in the world can read your API responses from inside your users' browsers. An attacker can build a page that silently calls your API using the user's existing session cookies (because the browser attaches them automatically), then exfiltrates the response to the attacker's server.
Common Mistakes
-
Setting CORS headers in the response body instead of HTTP headers. JSON that says
{ "Access-Control-Allow-Origin": "*" }does nothing. Headers must be in the HTTP response headers, not the body. -
Only setting CORS on the route, not the OPTIONS handler. The browser's preflight hits your
OPTIONSroute, which may be returning a 404 or 405 without CORS headers, even though yourGET/POSTroute has them. -
Setting different origins on different responses. If your CORS middleware sets the origin dynamically, make sure caching layers (CDN, nginx, browser) do not cache a response with the wrong
Access-Control-Allow-Origin. UseVary: Originto tell caches that the header varies by origin. -
Missing
Vary: Originwhen using dynamic origin matching. Without it, a CDN might cache a response that allowshttps://myapp.comand serve it to a request from a completely different origin, causing inconsistent behaviour. -
Applying a CORS fix in development only. If your CORS fix is in a
.env-gated code path, it will not be present in production. Always verify in a staging environment identical to prod.
Try It In Your Browser
Paste any API URL into the AllDevToolsHub CORS Header Checker to instantly see which CORS headers the server returns, test preflight responses, and validate your Access-Control-Allow-Origin values — no curl command, no Postman, no install required. All requests are made from your browser.
Frequently Asked Questions
Why does my API work in Postman but fail in the browser?
CORS is enforced entirely by the browser, not the server. Postman, curl, and backend services make direct HTTP requests that bypass the Same-Origin Policy. Only browser JavaScript is subject to CORS. If the API works in Postman and fails in your browser's network tab, add Access-Control-Allow-Origin to your server responses.
Can I fix CORS from the frontend?
No. CORS headers must be set by the server. The only thing you can do on the frontend is change how you make the request — for example, routing it through a server-side proxy that adds the correct headers. Browser extensions that "disable CORS" only affect your own browser and are not a solution for users.
What is the difference between simple and non-simple (preflighted) requests?
A "simple" request uses GET, HEAD, or POST with only application/x-www-form-urlencoded, multipart/form-data, or text/plain content types, and no custom headers. Everything else triggers a preflight OPTIONS request. In practice: if you send JSON (Content-Type: application/json) or an Authorization header, expect a preflight.
Why am I getting CORS errors on localhost?
Two common causes: (1) Your backend runs on http://localhost:3001 and your frontend on http://localhost:3000 — those are different origins (different port), so CORS applies. Add http://localhost:3000 to your allowed origins in development. (2) The Private Network Access (CORS-RFC1918) policy — if you are accessing a local server from a public URL, Chrome sends an extra Access-Control-Request-Private-Network: true header and requires Access-Control-Allow-Private-Network: true back.
Should I use Access-Control-Allow-Origin: * in production?
Only for fully public, anonymous, read-only APIs. Never use * on any endpoint that handles authenticated requests, user data, or mutations. Use the exact origin of your frontend app instead, and set Vary: Origin so caches handle the header correctly.
The CORS error pyramid is short: almost every real-world CORS bug is either a missing Access-Control-Allow-Origin header, a * + credentials conflict, or a preflight response that does not list the required method or header. Run the CORS Header Checker to diagnose in seconds, then apply the framework-specific fix above.
For the full list of HTTP response codes your server might return during a failed preflight, see the HTTP Status Code Cheatsheet. For securing your API surface beyond CORS, read HTTP Security Headers: The 2026 Complete Checklist.
Top comments (0)