Cross-Origin Resource Sharing (CORS) is one of the most frequent sources of runtime confusion for frontend and backend engineers. When a browser blocks an API request due to CORS, it fails silently at the transport layer, rendering the response unreadable to client-side JavaScript while often returning a successful 200 OK on the server.
CORS is not an authorization mechanism; it is a browser security policy enforced to protect users against Cross-Site Request Forgery (CSRF) and unauthorized cross-domain reads.
This guide walks through the mechanics of simple vs. preflighted CORS requests, provides diagnostic workflows for the 5 most common CORS error messages, and outlines production configurations for Express, Next.js, FastAPI, and Nginx.
1. How CORS Works: Simple vs. Preflight Requests
The browser partitions cross-origin HTTP requests into two categories based on method, headers, and content type.
┌─────────────────────────────────────────────────────────────┐
│ Browser Outgoing Request │
└──────────────────────────────┬──────────────────────────────┘
│
Is it a Simple Request?
• Method: GET, HEAD, POST
• Headers: Accept, Accept-Language, Content-Language, Content-Type
• Content-Type: text/plain, multipart/form-data,
application/x-www-form-urlencoded
│
┌──────────────┴──────────────┐
▼ ▼
[ YES ] [ NO ]
Direct Request Sent Preflight Required
(Browser includes Origin) (Browser sends OPTIONS)
│ │
Server returns response + Server returns 200/204 +
Access-Control-Allow-Origin Access-Control-Allow-* headers
│ │
Browser grants data to JS Browser issues actual HTTP request
Simple Requests
A request is classified as "simple" if:
- It uses
GET,POST, orHEAD. - It only includes standard safe headers (
Accept,Accept-Language,Content-Language,Content-Type). - The
Content-Typeis strictlyapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain.
For simple requests, the browser sends the request immediately with an Origin header. If the server does not respond with a matching Access-Control-Allow-Origin, the browser blocks the JavaScript client from accessing the response body.
Preflighted Requests
Any request using application/json, custom headers (Authorization, X-API-Key, X-Trace-Id), or methods like PUT, PATCH, or DELETE triggers a preflight OPTIONS check before the actual request is sent.
OPTIONS /api/v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.wtool.dev
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
The server must respond with a 200 OK or 204 No Content containing the allowed origins, methods, headers, and cache max-age:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.wtool.dev
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
2. Top 5 CORS Error Messages & How to Fix Them
Error 1: No 'Access-Control-Allow-Origin' header is present on the requested resource
- Root Cause: The server did not attach
Access-Control-Allow-Originto the response, or an unhandled server error (such as a500 Internal Server Errorbefore CORS middleware executed) caused the response to bypass CORS header injection. - Fix: Ensure CORS middleware runs as the first middleware in your pipeline, before routing or error handlers.
Error 2: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
- Root Cause: The API server or reverse proxy received an
OPTIONSrequest and returned a401 Unauthorized,403 Forbidden, or404 Not Found(common when auth middleware intercepts preflight requests). - Fix: Whitelist
OPTIONSrequests from authentication guards. Preflight requests never carry auth cookies orAuthorizationheaders.
Error 3: 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'
- Root Cause: The client request was made with
credentials: 'include'(cookies or HTTP auth), but the server responded withAccess-Control-Allow-Origin: *. - Fix: When credentials are enabled (
Access-Control-Allow-Credentials: true), the server must echo back the exact requesting origin rather than*.
// ❌ WRONG (Browser rejects credentialed response)
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ✅ CORRECT (Dynamically validate and echo origin)
const allowedOrigins = ['https://app.wtool.dev', 'https://staging.wtool.dev'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
Error 4: Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers in preflight response
- Root Cause: The client included a custom header (e.g.
X-Request-Id,Sentry-Trace,baggage) that was not listed in the server'sAccess-Control-Allow-Headersheader. - Fix: Explicitly include all application headers or use wildcard headers if supported.
Error 5: Method PATCH is not allowed by Access-Control-Allow-Methods
- Root Cause: The preflight response lacked the HTTP verb used in the subsequent request.
- Fix: Add
PATCH,PUT,DELETEto theAccess-Control-Allow-Methodsresponse header.
3. Production CORS Configurations
Node.js (Express)
import express from 'express';
import cors from 'cors';
const app = express();
const whitelist = ['https://app.wtool.dev', 'https://admin.wtool.dev'];
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
// Allow non-browser tools (cURL, Postman) where origin is undefined
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
credentials: true,
maxAge: 86400, // Cache preflight for 24 hours
optionsSuccessStatus: 204,
};
app.use(cors(corsOptions));
Next.js App Router (app/api/[...route]/route.ts)
import { NextResponse, type NextRequest } from 'next/server';
const ALLOWED_ORIGIN = 'https://app.wtool.dev';
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
export async function POST(request: NextRequest) {
const data = await request.json();
return NextResponse.json({ success: true, data }, {
headers: {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
},
});
}
Nginx Reverse Proxy
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://backend_upstream;
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
4. Debugging CORS via cURL
Because terminal HTTP clients bypass browser same-origin checks, simulate browser behavior by manually specifying Origin and preflight headers:
# Simulate a preflight OPTIONS check
curl -X OPTIONS https://api.example.com/v1/users \
-H "Origin: https://app.wtool.dev" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization, Content-Type" \
-i
Look for the presence of:
Access-Control-Allow-Origin: https://app.wtool.devAccess-Control-Allow-Methods: ... POST ...Access-Control-Allow-Headers: ... Authorization ...
Tip: Analyze complete response headers, check security flags, and verify CORS headers instantly using the DevFlow HTTP Headers Analyzer.