Fix CORS Preflight Request Failed — Complete Troubleshooting Guide
The CORS preflight request failed error means the browser blocked your cross-origin request because the server didn't respond correctly to the OPTIONS preflight check. Here's how to fix it.
- 1. Open DevTools → Network tab → find the failed OPTIONS request
- 2. Check which
Access-Control-*header is missing - 3. Add the missing headers on the server (see examples below)
What Is a CORS Preflight Request?
When your JavaScript makes a cross-origin request that is not "simple," the browser automatically sends an OPTIONS request first. This is the preflight. The server must respond with the correct Access-Control-* headers. If it doesn't, the browser blocks the actual request and you see the error.
When Is a Preflight Triggered?
A preflight is sent when your request is not a "simple request." A request is simple only if all of these are true:
Triggers preflight: PUT, PATCH, DELETE, etc. trigger preflight
Triggers preflight: application/json triggers preflight
Triggers preflight: Authorization, X-API-Key, etc. trigger preflight
Triggers preflight: credentials: "include" with Access-Control-Allow-Origin: * fails
Common Causes of Preflight Failure
Your server returns 405 Method Not Allowed or 404 for OPTIONS. The browser needs a 200 or 204 response with the correct headers.
You send Authorization or Content-Type: application/json, but the server's Allow-Headers doesn't list them. Every custom header must be explicitly allowed.
When using credentials: "include", the origin cannot be a wildcard. You must return the specific origin (e.g., https://frontend.com) and set Access-Control-Allow-Credentials: true.
In Express, if your CORS middleware is placed after your routes, the route handler responds to OPTIONS before the CORS middleware can add headers. Always put CORS middleware first.
Some servers (especially with multiple middleware) add duplicate Allow-Origin headers. The browser rejects this. Ensure only one Allow-Origin header is set.
If the OPTIONS request gets redirected (301/302), the browser cancels the preflight. Ensure your API endpoint doesn't redirect OPTIONS requests.
Server-Side Fixes
Express (Node.js)
// Option 1: Use the cors middleware (recommended)
const cors = require('cors');
app.use(cors({
origin: 'https://frontend.com', // or an array of origins
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
// Option 2: Manual headers
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://frontend.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400'); // cache preflight 24h
res.status(204).end();
});Important: Place the CORS middleware before your routes. If it runs after, the route handler intercepts the OPTIONS request first.
Nginx
server {
listen 80;
server_name api.example.com;
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://frontend.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
location /api/ {
# Add CORS headers to actual responses
add_header 'Access-Control-Allow-Origin' 'https://frontend.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://backend:3000;
}
}Apache (.htaccess)
# Handle preflight OPTIONS
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
# CORS headers
Header set Access-Control-Allow-Origin "https://frontend.com"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
Header set Access-Control-Allow-Credentials "true"
Header set Access-Control-Max-Age "86400"Debugging with Browser DevTools
Dynamic Origin Handling
If your API serves multiple frontends, you need to dynamically set the allowed origin:
// Express: allow multiple origins dynamically
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000', // dev
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));Frequently Asked Questions
Why does my API work in Postman but fails with CORS in the browser?
Postman and curl don't enforce CORS — they're not browsers. CORS is a browser-only security mechanism. The browser sends a preflight OPTIONS request that your server must handle. Postman sends the actual request directly. If your server doesn't respond to OPTIONS with the right headers, the browser blocks the request, but Postman works fine.
Can I use Access-Control-Allow-Origin: * for everything?
Only for public APIs without authentication. You cannot use * with credentials (cookies, Authorization headers). When using credentials: "include" in fetch, you must specify the exact origin. Also, * does not work if the request has custom headers — the browser still requires an explicit Allow-Headers response.
What does Access-Control-Max-Age do?
It tells the browser how long (in seconds) to cache the preflight response. During this time, the browser won't send another OPTIONS request for the same endpoint. A value of 86400 (24 hours) is common. This reduces latency for repeated cross-origin requests.
How do I fix CORS on localhost during development?
Add http://localhost:PORT to your allowed origins list. For Express: app.use(cors({ origin: ['http://localhost:3000', 'http://localhost:5173'] })). Never use * in production if you need credentials. For development only, you can also use a browser extension like "CORS Unblock" — but never rely on this in production.
Key Takeaways
- A preflight is an automatic OPTIONS request the browser sends before non-simple cross-origin requests.
- Your server must respond to OPTIONS with the correct
Access-Control-*headers. - Every custom header in the request must be listed in
Access-Control-Allow-Headers. - You cannot use
*origin with credentials — specify the exact origin. - Use DevTools Network tab to see exactly which header is missing from the OPTIONS response.