How to Fix CORS Errors in the Browser

Type: Software Reference Confidence: 0.95 Sources: 8 Verified: 2026-02-23 Freshness: quarterly

TL;DR

Constraints

Quick Reference

# Error / Scenario Root Cause Fix
1 No 'Access-Control-Allow-Origin' header Server doesn't send the header Add Access-Control-Allow-Origin to server response [src1, src3]
2 Origin not allowed Origin not in server's allowlist Add specific origin to allowlist [src1, src4]
3 Preflight (OPTIONS) fails Server doesn't handle OPTIONS Return CORS headers on OPTIONS with 204 [src1, src6]
4 Wildcard + credentials * can't be used with credentials Echo specific origin; set Vary: Origin [src1, src4]
5 Custom header blocked Header not in Allow-Headers Add to Access-Control-Allow-Headers [src1, src3]
6 Method blocked Method not in Allow-Methods Add to Access-Control-Allow-Methods [src1]
7 Cookie not sent cross-origin credentials: 'include' missing Add on client + Allow-Credentials: true on server [src1, src4]
8 Trailing slash mismatch https://example.com/https://example.com Strip trailing slash from origin comparison [src3, src5]
9 HTTP vs HTTPS mismatch Protocol is part of origin Ensure both sides use same protocol [src2, src3]
10 Port mismatch Port is part of origin Include port in allowed origins [src2, src3]
11 Redirect changes origin Redirect to different origin Handle CORS on redirect target too [src1, src6]
12 Private Network Access blocked Public site accessing private IP Add Access-Control-Allow-Private-Network: true to preflight response [src7]
13 Authorization header not allowed * wildcard doesn't cover Authorization Explicitly list Authorization in Access-Control-Allow-Headers [src1, src6]

Decision Tree

START — Browser shows CORS error
├── DevTools → Network → find the failed request
│   ├── Preflight OPTIONS request present?
│   │   ├── YES — OPTIONS fails (4xx or no CORS headers)
│   │   │   ├── 404/405 on OPTIONS → add OPTIONS handler [src1, src6]
│   │   │   ├── "No Access-Control-Allow-Private-Network" → add PNA header [src7]
│   │   │   └── OPTIONS ok but actual fails → check Allow-Methods/Headers [src1]
│   │   └── NO — Simple request
│   │       ├── No Access-Control-Allow-Origin → add to server [src1, src3]
│   │       ├── Wrong origin → fix allowlist [src1, src4]
│   │       └── Has * but credentials → echo specific origin [src1, src4]
├── Request includes cookies / Authorization?
│   ├── YES → specific origin (not *) + Allow-Credentials: true
│   │         + explicit Authorization in Allow-Headers [src1, src4, src6]
│   └── NO → Wildcard * is fine for public APIs [src1, src5]
├── Custom header blocked (Authorization, X-API-Key)?
│   └── Add to Access-Control-Allow-Headers (Authorization always explicit) [src1, src3]
├── Public site accessing localhost / private IP?
│   └── Add Access-Control-Allow-Private-Network: true [src7]
└── Development only? → Use dev proxy (Vite/webpack) [src3, src5]

Step-by-Step Guide

1. Diagnose the exact error

Read the browser console — the message tells you exactly what's missing. [src1, src3]

# Common messages:
"No 'Access-Control-Allow-Origin' header is present"
→ Server doesn't send the header at all

"The 'Access-Control-Allow-Origin' header has a value that is not equal to the supplied origin"
→ Server sends wrong origin

"Response to preflight request doesn't pass access control check"
→ Server doesn't handle OPTIONS preflight

"The value of 'Access-Control-Allow-Origin' must not be '*' when credentials mode is 'include'"
→ Wildcard + credentials conflict

"No 'Access-Control-Allow-Private-Network' header was present in the preflight response"
→ Private Network Access preflight missing [src7]

2. Fix on the server — Express.js (Node.js)

The cors npm package is the standard solution. [src1, src3, src4]

const cors = require('cors');
const allowedOrigins = ['https://app.example.com', 'https://www.example.com'];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
    callback(new Error(`CORS: origin ${origin} not allowed`));
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,
}));
app.options('*', cors());  // Handle preflight for all routes

3. Fix on the server — nginx

Configure CORS headers in nginx for any backend. [src1, src3]

location /api/ {
    set $cors_origin "";
    if ($http_origin ~* "^https://(app|www)\.example\.com$") {
        set $cors_origin $http_origin;
    }
    add_header 'Access-Control-Allow-Origin' $cors_origin always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
    add_header 'Vary' 'Origin' always;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' '86400';
        add_header 'Content-Length' '0';
        return 204;
    }
    proxy_pass http://backend:8080;
}

4. Fix on the server — Python (FastAPI / Flask)

Standard CORS middleware for Python frameworks. [src1, src4]

# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
    max_age=86400)

# Flask
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"],
     supports_credentials=True,
     allow_headers=["Content-Type", "Authorization"])

5. Development proxy (Vite / webpack)

Never disable browser security. Use a dev proxy instead. [src3, src5]

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
};

6. Client-side: sending credentials correctly

Credentials must be explicitly requested on the client. [src1, src4]

// Include cookies and auth headers cross-origin
fetch('https://api.example.com/data', {
  credentials: 'include',
  headers: { 'Authorization': 'Bearer token123' },
});

// axios
axios.get('https://api.example.com/data', { withCredentials: true });

7. Handle Private Network Access (Chrome 130+)

Public websites accessing localhost/private IPs need PNA headers. [src7]

// Express middleware for Private Network Access
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.headers['access-control-request-private-network'] === 'true') {
    res.header('Access-Control-Allow-Private-Network', 'true');
  }
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Code Examples

Express.js — production CORS middleware

const express = require('express');
const cors = require('cors');
const app = express();

const allowedOrigins = [
  'https://app.example.com',
  'https://www.example.com',
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin) return callback(null, true);
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS: origin ${origin} not allowed`));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,
}));

app.options('*', cors());

app.get('/api/data', (req, res) => {
  res.json({ data: 'hello' });
});

nginx — dynamic origin allowlist with preflight

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        set $cors_origin "";
        if ($http_origin ~* "^https://(app|www)\.example\.com$") {
            set $cors_origin $http_origin;
        }

        add_header 'Access-Control-Allow-Origin' $cors_origin always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
        add_header 'Access-Control-Max-Age' '86400' always;
        add_header 'Vary' 'Origin' always;

        if ($request_method = 'OPTIONS') {
            add_header 'Content-Length' '0';
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }

        proxy_pass http://backend:8080;
    }
}

FastAPI — full CORS configuration

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://app.example.com",
        "https://www.example.com",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization", "X-API-Key"],
    max_age=86400,
)

@app.get("/api/data")
async def get_data():
    return {"data": "hello"}

Express.js — Private Network Access handler

// For local dev servers accessed from public websites (Chrome 130+)
const express = require('express');
const app = express();

app.use((req, res, next) => {
  const origin = req.headers.origin;
  res.header('Access-Control-Allow-Origin', origin || '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // Handle Private Network Access preflight
  if (req.headers['access-control-request-private-network'] === 'true') {
    res.header('Access-Control-Allow-Private-Network', 'true');
  }

  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Anti-Patterns

Wrong: Wildcard with credentials


Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Correct: Echo specific origin with credentials

// GOOD — reflect the specific requesting origin [src1, src4]
app.use(cors({
  origin: (origin, cb) => cb(null, allowedOrigins.includes(origin)),
  credentials: true,
}));
// Response: Access-Control-Allow-Origin: https://app.example.com
//           Access-Control-Allow-Credentials: true
//           Vary: Origin

Wrong: Disabling browser security for development

# BAD — dangerous, masks real issues [src3, src5]
chrome.exe --disable-web-security --user-data-dir="%TEMP%\chrome_dev"

Correct: Use a dev proxy

// GOOD — vite.config.js [src3, src5]
export default {
  server: {
    proxy: {
      '/api': { target: 'https://api.example.com', changeOrigin: true }
    }
  }
};

Wrong: Adding CORS headers on the client

// BAD — CORS headers are response headers; client can't set them [src1, src2]
fetch(url, {
  headers: {
    'Access-Control-Allow-Origin': '*',  // Does nothing!
  }
});

Correct: Fix the server response headers

// GOOD — CORS is always fixed on the server [src1, src2]
// In Express: app.use(cors({ origin: 'https://myapp.com' }))
// In nginx: add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;

Wrong: Using fetch mode 'no-cors' to silence errors

// BAD — gives opaque response you can't read [src1, src6]
const response = await fetch('https://api.example.com/data', {
  mode: 'no-cors',  // Silences error but response.json() will fail
});
// response.type === 'opaque', status === 0, body unreadable

Correct: Fix CORS on the server or use a proxy

// GOOD — fix the server, or proxy through your own backend [src1, src3]
// Option 1: Fix server CORS headers
// Option 2: Proxy through your own backend
const response = await fetch('/api/proxy/data');  // Same-origin, no CORS needed

Common Pitfalls

Diagnostic Commands

# Test CORS headers with curl (simple request)
curl -H "Origin: https://app.example.com" \
     -v https://api.example.com/data 2>&1 | grep -i "access-control"

# Test preflight request
curl -X OPTIONS \
     -H "Origin: https://app.example.com" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type, Authorization" \
     -v https://api.example.com/data 2>&1 | grep -i "access-control"

# Test Private Network Access preflight (Chrome 130+)
curl -X OPTIONS \
     -H "Origin: https://public-site.com" \
     -H "Access-Control-Request-Method: GET" \
     -H "Access-Control-Request-Private-Network: true" \
     -v http://localhost:3000/api 2>&1 | grep -i "access-control"

# Check all response headers
curl -I -H "Origin: https://app.example.com" https://api.example.com/data

# Browser DevTools steps:
# 1. F12 → Network tab
# 2. Find failed request (red)
# 3. Click → Headers → Response Headers
# 4. Look for: Access-Control-Allow-Origin, Access-Control-Allow-Credentials
# 5. Find OPTIONS preflight above the main request (if any)

Server Configuration Quick Reference

Express (Node.js):    npm install cors → app.use(cors({ origin: '...', credentials: true }))
FastAPI (Python):     CORSMiddleware(allow_origins=[...], allow_credentials=True)
Flask (Python):       pip install flask-cors → CORS(app, origins=[...], supports_credentials=True)
Django (Python):      pip install django-cors-headers → CORS_ALLOWED_ORIGINS = [...]
Spring Boot (Java):   @CrossOrigin(origins = "...") or global CorsConfigurationSource bean
ASP.NET Core (C#):    builder.Services.AddCors(...) → app.UseCors("MyPolicy")
nginx:                add_header 'Access-Control-Allow-Origin' '...' always; add_header 'Vary' 'Origin' always;
Apache:               Header always set Access-Control-Allow-Origin "..."

Version History & Compatibility

Version / Standard Behavior Notes
Fetch Living Standard (current) Full CORS credentials: 'include' replaces XHR withCredentials; wildcard * for Allow-Headers does not cover Authorization [src6]
Chrome 130+ (2024-10) Private Network Access enforced Public-to-private requests require Access-Control-Allow-Private-Network: true preflight [src7]
Chrome 80+ (2020) SameSite=None required Cross-origin cookies need SameSite=None; Secure [src1]
Chrome 66+ (2018) Preflight caching limited Access-Control-Max-Age capped at 7200s (2h) in Chrome [src1]
Safari 12+ (2018) Full CORS ITP may block third-party cookies even with CORS [src1]
Firefox 3.5+ (2009) Full CORS First browser to implement CORS [src1]
IE 10+ (2012) XDomainRequest deprecated Use standard Fetch/XHR [src1]
RFC 6454 (2011) Origin header Defines the Origin header format [src2]

When to Use / When Not to Use

Use When Don't Use When Use Instead
Browser JS calls a different domain/port/protocol Server-to-server calls No CORS needed for server-to-server
API needs cross-origin browser access Same-origin requests No CORS config needed
Need cookies/auth cross-origin Using CDN for static assets CDN doesn't need CORS for HTML/CSS/JS (but does for Fetch/XHR)
Embedding resources from another domain Mobile apps (React Native) Mobile apps don't enforce CORS
Public site accessing localhost/private IPs WebSocket connections WebSockets don't use CORS (different security model)

Important Caveats

Related Units