Fix: CORS Not Working in Express (Access-Control-Allow-Origin Missing)
Quick Answer
How to fix CORS errors in Express.js — cors middleware not applying, preflight OPTIONS requests failing, credentials not allowed, and specific origin whitelisting issues.
The Error
Your Express API returns responses, but the browser blocks them with:
Access to fetch at 'http://localhost:3000/api/users' from origin 'http://localhost:5173'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.Or for requests with credentials:
Access to XMLHttpRequest at 'http://api.example.com/data' from origin 'http://app.example.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'.Or for preflight requests:
Response to preflight request doesn't pass access control check:
It does not have HTTP ok status.The API works fine with curl or Postman — only browser requests are blocked.
Why This Happens
CORS (Cross-Origin Resource Sharing) is enforced by the browser, not the server. When a browser makes a request to a different origin (different domain, protocol, or port), it checks the response headers to decide whether to allow the JavaScript code to read the response.
Common causes in Express:
corsmiddleware not installed or not applied before route handlers.- Middleware order —
cors()added after the routes it should cover. - Preflight OPTIONS requests not handled — the browser sends a
OPTIONSrequest first for non-simple requests, and Express returns 404 if no handler exists. - Wildcard
*with credentials —Access-Control-Allow-Origin: *cannot be used withcredentials: 'include'. - Wrong origin in whitelist — trailing slash, wrong port, or http vs https mismatch.
- CORS headers on error responses — if your error handler runs before cors middleware, error responses (4xx, 5xx) lack CORS headers and the browser blocks them.
Fix 1: Install and Apply cors Middleware Correctly
The cors npm package is the standard solution. Apply it before your routes:
npm install cors
npm install -D @types/cors # TypeScriptconst express = require("express");
const cors = require("cors");
const app = express();
// Apply BEFORE all routes
app.use(cors());
app.get("/api/users", (req, res) => {
res.json({ users: [] });
});
app.listen(3000);Verify middleware order — this is the most common mistake:
// BROKEN — cors applied after the route
app.get("/api/users", handler);
app.use(cors()); // Too late — /api/users already has no CORS headers
// FIXED — cors applied before all routes
app.use(cors());
app.get("/api/users", handler);Common Mistake: Adding
app.use(cors())at the bottom of the file after all routes are defined. Express middleware runs in order — CORS headers are only added to routes defined afterapp.use(cors()).
Fix 2: Configure Specific Origins (Do Not Use * in Production)
cors() with no options allows all origins (*). For production, restrict to your actual frontend origins:
const corsOptions = {
origin: "https://app.example.com", // Single allowed origin
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
};
app.use(cors(corsOptions));Allow multiple origins:
const allowedOrigins = [
"https://app.example.com",
"https://www.example.com",
"http://localhost:5173", // Local dev
"http://localhost:3000",
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (curl, Postman, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
}));Allow origins by pattern (subdomains):
app.use(cors({
origin: (origin, callback) => {
if (!origin || /\.example\.com$/.test(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
}));Fix 3: Handle Preflight OPTIONS Requests
For non-simple requests (custom headers, PUT/DELETE/PATCH methods, Content-Type: application/json), the browser sends a preflight OPTIONS request before the actual request. Express must respond to it with 200 OK and the correct CORS headers.
The cors package handles this automatically when applied globally:
app.use(cors(corsOptions));
// OPTIONS requests are now handled for all routesIf you apply cors per-route, add explicit OPTIONS handling:
// Per-route cors
app.get("/api/users", cors(corsOptions), getUsers);
app.post("/api/users", cors(corsOptions), createUser);
// Must also handle OPTIONS preflight for each path
app.options("/api/users", cors(corsOptions));
// Or handle all OPTIONS requests globally
app.options("*", cors(corsOptions));If you are not using the cors package, set headers manually:
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "https://app.example.com");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.header("Access-Control-Max-Age", "86400"); // Cache preflight for 24h
if (req.method === "OPTIONS") {
return res.sendStatus(204); // No content — preflight response
}
next();
});Fix 4: Fix Credentials with CORS
When using credentials: 'include' in fetch (to send cookies or HTTP auth), two additional requirements apply:
Access-Control-Allow-Originmust be the specific origin, not*.Access-Control-Allow-Credentials: truemust be set.
Broken — wildcard with credentials:
app.use(cors()); // Sets Access-Control-Allow-Origin: *// Frontend — fails with credentials error
fetch("http://localhost:3000/api/profile", {
credentials: "include", // Sends cookies
});Fixed:
app.use(cors({
origin: "http://localhost:5173", // Specific origin — not *
credentials: true, // Sets Access-Control-Allow-Credentials: true
}));// Frontend
fetch("http://localhost:3000/api/profile", {
credentials: "include",
});For dynamic allowed origins with credentials:
app.use(cors({
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed"));
}
},
credentials: true,
}));Why this matters: The
*wildcard means “any origin can read this response.” Allowing credentials from any origin would let any website make authenticated requests on behalf of your users — a security risk. Browsers enforce this combination as an error.
Fix 5: Add CORS Headers to Error Responses
If your Express error handler runs independently of the cors middleware, error responses (400, 401, 500) may lack CORS headers. The browser then blocks the error response, and your frontend only sees a network error — not the actual error status or message.
Broken — CORS headers missing on errors:
app.use(cors()); // Adds CORS to normal responses
app.get("/api/data", (req, res) => {
throw new Error("Something went wrong");
});
// Global error handler — runs without CORS middleware context
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
// No Access-Control-Allow-Origin header — browser blocks this
});Fixed — apply cors inside the error handler too:
const corsMiddleware = cors(corsOptions);
app.use((err, req, res, next) => {
corsMiddleware(req, res, () => {
res.status(err.status || 500).json({ error: err.message });
});
});Or simply apply cors before the error handler and ensure it handles all responses:
app.use(cors(corsOptions)); // This actually works for errors too in most setups
// Route that throws
app.get("/api/data", asyncHandler(async (req, res) => {
throw new Error("Oops");
}));
app.use((err, req, res, next) => {
// CORS headers are already set by the middleware above
res.status(500).json({ error: err.message });
});Fix 6: Fix CORS Behind a Reverse Proxy (nginx)
If nginx sits in front of Express, CORS headers can be added at the nginx level. But if both nginx and Express add CORS headers, the browser may reject the duplicate headers:
The 'Access-Control-Allow-Origin' header contains multiple values
'https://app.example.com, https://app.example.com', but only one is allowed.Fix — add CORS in one place only:
Either handle CORS in nginx (and remove it from Express):
location /api/ {
proxy_pass http://localhost:3000/;
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}Or handle CORS in Express only (and remove nginx CORS directives). For most setups, handling it in Express is simpler and keeps the logic in code rather than server config.
For general nginx proxy issues, see Fix: Nginx 502 Bad Gateway.
Fix 7: Debug CORS Issues
Check the actual response headers:
curl -I -X OPTIONS \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
http://localhost:3000/api/usersThe response should include:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationLog the origin in development:
app.use((req, res, next) => {
console.log("Origin:", req.headers.origin);
next();
});
app.use(cors(corsOptions));Verify the origin your frontend sends exactly matches what you have in the whitelist — a missing port, http vs https, or trailing slash causes a mismatch.
Still Not Working?
Check for Access-Control-Allow-Origin already set elsewhere. If a framework or proxy is setting this header before your cors middleware, you may have duplicate values.
Check the browser’s Network tab. In Chrome DevTools → Network → select the failed request → Headers tab. Check the actual response headers sent, and the error in the Console tab. The browser gives a specific reason for each CORS rejection.
Check for redirect loops. If your Express app redirects the request (301/302), the redirected response also needs CORS headers. The cors package with app.use() covers all responses including redirects.
Check for HTTP vs HTTPS mismatch. http://localhost:5173 and https://localhost:5173 are different origins. Make sure your allowed origins list uses the correct protocol.
For CORS errors specific to credentials and cookies, see Fix: CORS Credentials Error. For preflight-specific issues, see Fix: CORS Preflight Request Blocked.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Access to fetch has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header
How to fix 'Access to fetch at ... from origin ... has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource' in JavaScript. Covers Express, Django, Flask, Spring Boot, ASP.NET, nginx, Apache, dev proxies, preflight requests, credentials, and edge cases.
Fix: Express req.body Is undefined
How to fix req.body being undefined in Express — missing body-parser middleware, wrong Content-Type header, middleware order issues, and multipart form data handling.
Fix: Stripe Webhook Signature Verification Failed
How to fix Stripe webhook signature verification errors — why Stripe-Signature header validation fails, how to correctly pass the raw request body, and how to debug webhook delivery in the Stripe dashboard.
Fix: CORS Error with Credentials – Access-Control-Allow-Credentials and Wildcard Origin
How to fix CORS errors when using cookies or Authorization headers, including 'Access-Control-Allow-Credentials' and wildcard origin conflicts.