Web APIs & Networking

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.

Quick Fix (3 steps)
  1. 1. Open DevTools → Network tab → find the failed OPTIONS request
  2. 2. Check which Access-Control-* header is missing
  3. 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.

// Browser automatically sends this first:
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
// Server must respond with:
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

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:

Simple if: Method is GET, HEAD, or POST

Triggers preflight: PUT, PATCH, DELETE, etc. trigger preflight

Simple if: Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain

Triggers preflight: application/json triggers preflight

Simple if: No custom headers

Triggers preflight: Authorization, X-API-Key, etc. trigger preflight

Simple if: No credentials with wildcard origin

Triggers preflight: credentials: "include" with Access-Control-Allow-Origin: * fails

Common Causes of Preflight Failure

Server doesn't handle OPTIONS requests

Your server returns 405 Method Not Allowed or 404 for OPTIONS. The browser needs a 200 or 204 response with the correct headers.

Missing Access-Control-Allow-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.

Access-Control-Allow-Origin is * with credentials

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.

CORS middleware runs after route handlers

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.

Multiple Access-Control-Allow-Origin headers

Some servers (especially with multiple middleware) add duplicate Allow-Origin headers. The browser rejects this. Ensure only one Allow-Origin header is set.

Redirect during preflight

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

1.Open DevTools (F12) → Network tab
2.Trigger the request that fails. You'll see two entries: the OPTIONS request (red) and the actual request (blocked).
3.Click the OPTIONS request → check the Response Headers. Look for missing or incorrect Access-Control-* headers.
4.Compare the request's Access-Control-Request-Headers with the response's Access-Control-Allow-Headers. Every requested header must be listed.
5.Check the status code. If it's 405 or 404, the server isn't handling OPTIONS at all.
6.Check the Console tab for the specific CORS error message — it tells you exactly which header is missing.

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.

Related Resources