Fix CORS Errors in 12 Minutes: Complete Debug Checklist

Solve Cross-Origin Resource Sharing errors with a systematic checklist covering headers, credentials, and preflight requests.

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 SameSite attribute on cookie (needs SameSite=None; Secure for 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:3000http://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:

  1. Open DevTools → Network tab
  2. Reload page
  3. Check request - should be green/200
  4. Verify Response Headers include Access-Control-Allow-*

Quick Reference Table

SymptomCauseFix
"No Allow-Origin header"Server not sending headerAdd CORS middleware
OPTIONS returns 404/405Server doesn't handle OPTIONSAdd app.options('*', cors())
"Wildcard not allowed"Using * with credentialsUse specific origin
Works in Postman, not browserCORS only enforced by browserAdd CORS headers to server
Cookies not sentMissing credentials settingAdd 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-Methods and Allow-Headers
  • Cannot use * origin with credentials: 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