Cybersecurity / Networking

CORS Error Despite Headers Present (Preflight OPTIONS Request Failing)

When your actual API endpoint clearly returns correct CORS headers, but the browser still blocks the request with a CORS error, the real problem is almost always in the separate preflight OPTIONS request — which browsers send automatically before certain requests, and which needs its own correct handling entirely independent of your main endpoint's own headers.

The Problem

Testing your endpoint directly with curl or Postman shows correct CORS headers in the response, yet the browser console shows:

Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com' 
has been blocked by CORS policy: Response to preflight request doesn't pass access 
control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The message specifically mentions "preflight request," which is the key detail — the actual endpoint you're checking manually isn't the request that's failing.

Why It Happens

For certain types of cross-origin requests (those using methods other than simple GET/POST, or including custom headers like Authorization), browsers automatically send a preliminary OPTIONS request — the "preflight" — to check whether the actual request will be allowed, before sending the real request at all. This preflight request is entirely separate from your main endpoint, and needs its own correct CORS response, independent of what your actual GET/POST/etc. handler returns. Common causes of the preflight specifically failing:

  • No route or handler exists at all for OPTIONS requests to that path, so the preflight gets a 404 or a generic error response with no CORS headers, rather than the expected 200 response with the right headers
  • A route does exist for OPTIONS, but it's handled by middleware or authentication logic that rejects it (since a preflight request never includes credentials or custom auth headers the way the real, subsequent request will), producing a 401/403 with no CORS headers rather than a successful preflight response
  • CORS middleware is correctly applied to your actual endpoint's route, but not applied broadly enough to also cover the automatically-generated OPTIONS preflight route for the same path
  • The preflight response is missing Access-Control-Allow-Methods or Access-Control-Allow-Headers, which are required specifically on the preflight response to tell the browser which methods and headers are permitted for the actual follow-up request

The Fix

First, confirm the preflight is actually the failing request, not your main endpoint, by checking the browser's network tab specifically for a separate OPTIONS entry for the same URL, and inspecting its response status and headers directly rather than only testing the main endpoint:

curl -X OPTIONS -i https://api.example.com/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization"

This directly simulates what the browser sends during a preflight, letting you see the actual response your server produces for it, in isolation from your main endpoint.

Ensure your CORS middleware is applied broadly enough to also handle OPTIONS requests, not just the main HTTP methods your endpoint actually uses. Most CORS middleware libraries handle this automatically if configured correctly, but manual or custom CORS handling can miss it:

// Example: Express with the cors package - handles OPTIONS automatically
app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

If you're implementing CORS manually rather than through a library, explicitly handle OPTIONS requests before any authentication middleware runs, since a preflight request never carries real credentials and should never be rejected by an auth check:

app.use((req, res, next) => {
  if (req.method === 'OPTIONS') {
    res.header('Access-Control-Allow-Origin', 'https://app.example.com');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    return res.sendStatus(200);
  }
  next();
});
// authentication middleware comes after this point

Confirm the preflight response includes all three commonly required headers together — Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers — since a preflight missing any one of these, even if the others are present, causes the browser to block the subsequent real request.

Still Not Working?

If the preflight response looks entirely correct when tested directly with curl, but the browser still blocks the request, check whether a reverse proxy, API gateway, or CDN sitting in front of your application is intercepting OPTIONS requests before they reach your actual application code — some infrastructure layers have their own default handling for OPTIONS that returns a generic response without the CORS headers your application would have otherwise correctly added, meaning the fix in that case needs to happen at that infrastructure layer's own configuration, not in your application code at all.