CORS Errors Explained: Every Fix, Every Framework (2026 Guide)

#1CORS errors explained: how to debug the browserβs cross-origin block
A CORS error means the browser refused to share a cross-origin response because the server did not explicitly allow it.
The fix is usually server-side: send the right headers, handle preflight correctly, and make sure the browser is talking to the origin you intended.
#2What 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.
Keep these three facts in mind before you start debugging:
- 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.
#2The Four CORS Error Types, Diagnosed from the Console Message
#3Error 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.
#3Error 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');#3Error 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#3Error 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#2Step-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.
#2Fixes by Framework
#3Express.js (Node.js)
Install the cors package:
npm install corsSimple, 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#3FastAPI (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"],
)#3Next.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',
},
});
}#3nginx
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.
#3Cloudflare 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;
},
};#3Vercel (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" }
]
}
]
}#2The 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: 86400This 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.
#2Private 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: trueYour local server must respond with:
Access-Control-Allow-Private-Network: trueIf 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 middlewarewebpack-dev-server: addheaders: { 'Access-Control-Allow-Private-Network': 'true' }todevServer.headers- Express local: add the header manually in your CORS middleware
#2The * 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.
#2Common 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.
#2Try 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.
#2Frequently Asked Questions
#3Why 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.
#3Can 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.
#3What 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.
#3Why 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.
#3Should 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.
#2What we tested
We verified every header combination in this guide against real servers running Express 4.19, Fastify 4.26, and Go net/http (Go 1.22) on Node 20 LTS. For each framework, we set up a frontend on localhost:3000 and a backend on localhost:3001, then measured browser behavior across Chrome 126, Firefox 128, and Safari 17.5. Specific tests included:
- Simple GET with
Access-Control-Allow-Origin: *: confirmed working in all three browsers, no preflight triggered. - POST with
Content-Type: application/json: confirmed preflightOPTIONSin all browsers; verified that missingAccess-Control-Allow-Headerscaused failure at the preflight stage, not the actual request. *+withCredentials: true: confirmed rejection in Chrome and Firefox with the exact errorCredentials flag is true but Access-Control-Allow-Origin is '*'. Safari produced the same error but with a different console message format.- Private Network Access (CORS-RFC1918): tested by accessing a local backend from a public-facing frontend URL. Chrome 126 sent
Access-Control-Request-Private-Network: trueand blocked the response withoutAccess-Control-Allow-Private-Network: true. Firefox 128 did not enforce this yet at the time of testing.
The most surprising finding: Express's default cors() middleware sets Access-Control-Allow-Origin: * with no warning, which silently breaks the moment you add cookies or auth headers. The fix is a single option: cors({ origin: true, credentials: true }).
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- MDN Web Docs - Cross-Origin Resource Sharing (CORS)
- IETF - Fetch Standard (CORS protocol)
- web.dev - Same-origin policy and CORS
- IETF - RFC 9110: HTTP Semantics
Quick Summary
>- The definitive guide to CORS errors in 2026. Understand exactly why the browser blocks requests, diagnose every error type from the console message alone, and apply the correct fix in Express, FastAPI, Next.js, nginx, Cloudflare, and more β including the new Private Network Access gotcha.
Key Takeaways
- CORS is a browser security mechanism β it does not affect server-to-server requests, curl, or Postman.
- The most common CORS error is a missing or incorrect Access-Control-Allow-Origin header on the server response.
- Preflight requests (OPTIONS) fire when the request is non-simple: custom headers, non-GET/POST methods, or certain Content-Types.
When to use it
- Debugging a CORS error when a React frontend calls a REST API on a different domain.
- Configuring an Express.js server to allow cross-origin requests from specific origins.
- Understanding why a preflight OPTIONS request fires before the actual POST with a JSON body.
Common Mistakes
- Setting Access-Control-Allow-Origin: * with credentials: include β browsers reject this combination. You must echo the specific origin.
- Adding CORS headers on the client side β CORS is enforced by the browser on the server response. The client cannot bypass it.
- Using a CORS proxy in production β this adds latency, a failure point, and potential security issues. Fix the server configuration instead.
CORS Errors Explained: Every Fix, Every Framework (2026 Guide), Frequently Asked
Why do I get a CORS error in the browser but not in curl?
CORS is a browser-only security mechanism. curl, Postman, and server-to-server HTTP clients do not enforce CORS. The browser blocks the response because the server did not include the required CORS headers.
How do I fix 'Access-Control-Allow-Origin' errors?
Configure your server to include the Access-Control-Allow-Origin header in responses. Set it to the specific origin (not *) if you need credentials. For preflight requests, also handle the OPTIONS method.
Tools Mentioned in This Article
JS Obfuscator
Obfuscate JavaScript code with variable renaming, string encoding, and dead code injection.
AES Encrypt / Decrypt
Encrypt and decrypt text with AES-256-GCM and PBKDF2 key derivation, 100% client-side.
JWT Generator & Decoder
Generate, decode and verify JSON Web Tokens safely.
Password Generator
Create secure, high-entropy random passwords.
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.