How to Fix CORS Error "No 'Access-Control-Allow-Origin' Header Is Present" on OPTIONS Verb
Quick answer
Your API works fine when tested with curl or Postman, but the browser blocks it with a missing CORS header error β and inspecting the network tab reveals the...
Your API works fine when tested with curl or Postman, but the browser blocks it with a missing CORS header error β and inspecting the network tab reveals the actual failing request is an OPTIONS request, not the GET or POST you intended to send. This is the classic signature of a server that has CORS headers configured for its normal routes but never set up to handle the automatic preflight OPTIONS request browsers send first.
The Problem
The browser console shows a CORS error, but the actual failing network request is specifically the preflight:
Access to fetch at 'https://api.yourapp.com/data' from origin 'https://app.yourapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.
Checking the network tab confirms it's the OPTIONS request itself that's failing, often with a 404 or 405, rather than your actual GET/POST request:
OPTIONS /data HTTP/1.1
HTTP/1.1 404 Not Found
Testing the actual endpoint directly with curl (which never sends a preflight at all) succeeds without any apparent issue, which is exactly why this problem is often invisible until you specifically test from a browser.
Why It Happens
For any cross-origin request that isn't a "simple" request (which includes virtually anything sending JSON, using PUT/DELETE, or setting custom headers), the browser automatically sends a preflight OPTIONS request before the real one, asking the server for permission. Your server needs to explicitly handle this OPTIONS request and respond with the appropriate CORS headers β it's a completely separate request from your actual route, and many server setups only add CORS headers to their normal route handlers, leaving OPTIONS unhandled entirely. This produces the error because:
- No route or handler exists for
OPTIONSat all on the specific path, so the server returns a 404 or 405 instead of a proper preflight response, and the browser interprets that failure as "no CORS permission granted." - CORS middleware is only applied to specific route handlers rather than globally or before routing, so it never gets a chance to intercept and answer the automatic
OPTIONSrequest. - A reverse proxy or API gateway in front of the actual application intercepts
OPTIONSrequests and returns its own default response before they ever reach your application's CORS-handling code. - Authentication middleware runs before CORS middleware in the request pipeline, rejecting the unauthenticated
OPTIONSpreflight request (which never carries credentials) before CORS headers are ever added.
The Fix
Ensure your CORS middleware is registered globally, before both routing and any authentication middleware, so it can intercept and correctly answer every OPTIONS request regardless of the path or whether the request is authenticated:
// Express example
const cors = require('cors');
const app = express();
app.use(cors({
origin: 'https://app.yourapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
})); // registered BEFORE routes and auth middleware
app.use(authMiddleware);
app.use('/data', dataRoutes);
Most CORS middleware libraries automatically handle OPTIONS requests correctly once registered this way, responding to the preflight without needing it to pass through the rest of your routing or authentication logic at all.
If you're not using a CORS library and handling headers manually, explicitly add a route or middleware that responds to OPTIONS for every relevant path:
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://app.yourapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(204);
});
If a reverse proxy or API gateway sits in front of your application, check whether it needs its own separate CORS configuration, or whether it should simply pass OPTIONS requests through to your application unmodified rather than intercepting them itself:
# nginx example β pass OPTIONS through instead of intercepting
location /data {
proxy_pass http://127.0.0.1:3000;
# let the application handle CORS, don't add competing headers here
}
Test the preflight directly to confirm the fix, independent of your actual application code, by simulating exactly what the browser sends:
curl -X OPTIONS https://api.yourapp.com/data \
-H "Origin: https://app.yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" -i
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.yourapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Still Not Working?
If the direct curl test above succeeds but the browser still shows the error, the actual request from the browser might differ subtly from your manual curl test β check exactly what headers and method the browser is actually requesting permission for in the real preflight, since a mismatch between what's allowed and what's actually being requested (an additional custom header your curl test didn't include, for example) will still fail even though a simpler manual test succeeds:
# In the browser's Network tab, inspect the actual OPTIONS request's
# Access-Control-Request-Headers and Access-Control-Request-Method values,
# then confirm your server's Access-Control-Allow-Headers covers every one of them
Align your server's allowed headers list to explicitly include everything the real browser request is actually asking for, rather than assuming your manual test accurately represents what the actual application is sending in practice.