Skip to content

Fix: CORS Not Working in Express (Access-Control-Allow-Origin Missing)

FixDevs ·

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:

  • cors middleware not installed or not applied before route handlers.
  • Middleware ordercors() added after the routes it should cover.
  • Preflight OPTIONS requests not handled — the browser sends a OPTIONS request first for non-simple requests, and Express returns 404 if no handler exists.
  • Wildcard * with credentialsAccess-Control-Allow-Origin: * cannot be used with credentials: '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  # TypeScript
const 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 after app.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 routes

If 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:

  1. Access-Control-Allow-Origin must be the specific origin, not *.
  2. Access-Control-Allow-Credentials: true must 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/users

The 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, Authorization

Log 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles