Problem: CORS Blocks Your API Calls
Your frontend can't reach the backend API. Browser console shows "Access to fetch at '...' has been blocked by CORS policy" even though the endpoint works in Postman.
You'll learn:
- Why CORS exists and when it triggers
- Systematic debugging approach for all CORS scenarios
- How to fix both simple and preflight request failures
Time: 12 min | Level: Intermediate
Why This Happens
Browsers enforce CORS (Cross-Origin Resource Sharing) as a security mechanism. When your JavaScript tries to fetch https://api.example.com from https://app.example.com, the browser requires the API server to explicitly permit this cross-origin request.
Common symptoms:
- Request works in Postman/curl but fails in browser
- Error mentions "Access-Control-Allow-Origin"
- OPTIONS requests failing before your actual request
- Credentials not being sent despite
credentials: 'include'
Key insight: CORS is enforced by the browser, not the server. The server must send correct headers to tell the browser "this is allowed."
Solution
Step 1: Identify the Request Type
CORS has two modes: simple requests (GET/POST with standard headers) and preflight requests (anything else).
// Simple request - no preflight
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
// Preflight request - browser sends OPTIONS first
fetch('https://api.example.com/data', {
method: 'DELETE', // Non-simple method triggers preflight
headers: {
'Authorization': 'Bearer token', // Custom header triggers preflight
'Content-Type': 'application/json'
}
});
Check your Network tab:
- See only one request? → Simple request issue
- See OPTIONS then your request? → Preflight issue
- OPTIONS fails? → Your main request never runs
Step 2: Debug Simple Requests
If you see your GET/POST in Network tab but it's red/blocked:
# Check response headers (use your actual URL)
curl -I https://api.example.com/data
Required header:
Access-Control-Allow-Origin: https://yourfrontend.com
# OR
Access-Control-Allow-Origin: *
Fix for Node.js/Express:
app.use((req, res, next) => {
// Allow specific origin (production)
res.header('Access-Control-Allow-Origin', 'https://yourfrontend.com');
// OR allow all (development only)
res.header('Access-Control-Allow-Origin', '*');
next();
});
Fix for Python/FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourfrontend.com"], # Specific origins
# allow_origins=["*"], # Development only
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
If it still fails:
- Error: "Wildcard in Allow-Origin when credentials true" → Use specific origin, not
* - Error: "Multiple Allow-Origin values" → Server is sending header twice, check middleware order
Step 3: Debug Preflight Requests
OPTIONS requests must return before your actual request runs.
Check Network tab OPTIONS request:
# Required response headers for OPTIONS
Access-Control-Allow-Origin: https://yourfrontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Fix for Node.js/Express:
const cors = require('cors');
app.use(cors({
origin: 'https://yourfrontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // If using cookies/auth
maxAge: 86400 // Cache preflight for 24 hours
}));
// Ensure OPTIONS returns 200
app.options('*', cors());
Fix for Python/FastAPI:
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourfrontend.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "X-Custom-Header"],
max_age=86400,
)
Fix for Rust/Actix:
use actix_cors::Cors;
let cors = Cors::default()
.allowed_origin("https://yourfrontend.com")
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"])
.allowed_headers(vec![
actix_web::http::header::CONTENT_TYPE,
actix_web::http::header::AUTHORIZATION,
])
.max_age(86400);
App::new()
.wrap(cors)
.service(your_routes)
Why this works: OPTIONS requests need explicit permission for methods and headers. The Access-Control-Allow-* headers tell the browser what's permitted.
Step 4: Fix Credentials Issues
If sending cookies or Authorization headers:
// Frontend MUST include credentials
fetch('https://api.example.com/data', {
method: 'POST',
credentials: 'include', // Critical for cookies
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
}
});
Backend must explicitly allow credentials:
// Node.js
app.use(cors({
origin: 'https://yourfrontend.com', // CANNOT be * with credentials
credentials: true
}));
If it fails:
- Error: "Wildcard in Allow-Origin not allowed" → Use specific origin when
credentials: true - Cookies not sent: Check
SameSiteattribute on cookie (needsSameSite=None; Securefor cross-origin)
Step 5: Check for Common Mistakes
// ❌ Wrong: Setting headers on frontend doesn't help
fetch(url, {
headers: {
'Access-Control-Allow-Origin': '*' // This does nothing
}
});
// ✅ Correct: Server must send CORS headers
// Backend code sets Access-Control-* headers
Other gotchas:
- CORS headers must be on every response (including errors)
- Localhost origins are different:
http://localhost:3000≠http://127.0.0.1:3000 - Nginx/CloudFlare can strip CORS headers - check proxy config
Verification
Test the fix:
# Check preflight response
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://yourfrontend.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
You should see:
Access-Control-Allow-Origin: https://yourfrontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
HTTP/1.1 200 OK
Browser test:
- Open DevTools → Network tab
- Reload page
- Check request - should be green/200
- Verify Response Headers include
Access-Control-Allow-*
Quick Reference Table
| Symptom | Cause | Fix |
|---|---|---|
| "No Allow-Origin header" | Server not sending header | Add CORS middleware |
| OPTIONS returns 404/405 | Server doesn't handle OPTIONS | Add app.options('*', cors()) |
| "Wildcard not allowed" | Using * with credentials | Use specific origin |
| Works in Postman, not browser | CORS only enforced by browser | Add CORS headers to server |
| Cookies not sent | Missing credentials setting | Add credentials: 'include' + backend setup |
What You Learned
- CORS is a browser security feature requiring server permission
- Simple requests need
Access-Control-Allow-Origin - Preflight requests need additional
Allow-MethodsandAllow-Headers - Cannot use
*origin withcredentials: true
Limitations:
- CORS doesn't protect your API - use authentication
- Some proxies (Nginx, CloudFlare) can interfere with CORS headers
- Older browsers (IE11) have incomplete CORS support
Development vs Production Config
Development (permissive):
// Allow all origins - local development only
cors({
origin: '*',
credentials: false
})
Production (restrictive):
// Whitelist specific domains
const allowedOrigins = [
'https://app.example.com',
'https://app.staging.example.com'
];
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
})
Tested on Chrome 131, Firefox 133, Safari 17.2 | Node.js 22.x, Python 3.12, Rust 1.75