---
# === IDENTITY ===
id: software/debugging/browser-cors-errors/2026
canonical_question: "How do I fix CORS errors in the browser?"
aliases:
  - "CORS error browser"
  - "Access-Control-Allow-Origin missing"
  - "CORS policy blocked"
  - "preflight request failed"
  - "CORS credentials wildcard"
  - "No Access-Control-Allow-Origin header"
  - "CORS fix server configuration"
  - "cross-origin request blocked"
  - "Access-Control-Allow-Private-Network"
  - "private network access CORS"
  - "Local Network Access permission"
  - "Chrome 142 LNA prompt"
entity_type: software_reference
domain: software > debugging > browser_cors_errors
region: global
jurisdiction: global
temporal_scope: 2015-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.95
freshness: stable
version: 2.1
first_published: 2026-02-18

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Chrome 142 / Edge 143 — Local Network Access permission prompt replaces PNA preflight model (2025-09)"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Access-Control-Allow-Origin: * cannot be combined with Access-Control-Allow-Credentials: true — if you need cookies/auth, echo the specific origin"
  - "CORS headers must be set on the SERVER response, never on the client request — adding Access-Control-Allow-Origin to fetch() headers does nothing"
  - "Error responses (4xx/5xx) must also include CORS headers — otherwise the browser blocks the error and you see a generic CORS failure instead of the real error"
  - "Chrome 142+ / Edge 143+ replaces PNA preflights with a Local Network Access permission prompt — public websites accessing localhost/private IPs now require user permission AND fetch with targetAddressSpace: 'local'"
  - "The Authorization header is never covered by the Access-Control-Allow-Headers wildcard (*) — it must always be listed explicitly"
  - "Cross-origin cookies require SameSite=None; Secure attributes (Chrome 80+) — without these, credentials: 'include' sends no cookies even with correct CORS headers"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Error is about mixed content (HTTP resource on HTTPS page) rather than cross-origin"
    use_instead: "Fix mixed content by serving all resources over HTTPS"
  - condition: "Error occurs in server-to-server calls (Node.js, Python, curl) — CORS is browser-only"
    use_instead: "Server-to-server calls are not subject to CORS; check firewall/DNS/TLS instead"
  - condition: "Error is about Content Security Policy (CSP) violations, not CORS"
    use_instead: "CSP and CORS are separate mechanisms — check Content-Security-Policy headers"

# === AGENT HINTS ===
inputs_needed:
  - key: environment
    question: "Is this happening in development (localhost) or production?"
    type: choice
    options: ["Development (localhost)", "Production"]
  - key: credentials
    question: "Does the request need to send cookies or Authorization headers cross-origin?"
    type: choice
    options: ["Yes — cookies or auth headers", "No — public API"]
  - key: server_framework
    question: "What server framework are you using?"
    type: choice
    options: ["Express/Node.js", "nginx", "FastAPI", "Flask", "Django", "Spring Boot", "ASP.NET Core", "Other/Unknown"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/browser-cors-errors/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors"
    - id: "software/debugging/nodejs-econnrefused/2026"
      label: "Node.js ECONNREFUSED"
  solves:
    - id: "software/debugging/kubernetes-crashloopbackoff/2026"
      label: "Kubernetes CrashLoopBackOff (when caused by CORS misconfiguration in ingress)"
  often_confused_with:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors (looks like CORS but is a TLS handshake failure)"

# === SOURCES (5-8 authoritative sources) ===
sources:
  - id: src1
    title: "MDN — Cross-Origin Resource Sharing (CORS)"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
    type: official_docs
    published: 2025-11-21
    reliability: authoritative
  - id: src2
    title: "MDN — Same-origin policy"
    author: Mozilla
    url: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src3
    title: "FreeCodeCamp — CORS Errors and How to Fix Them"
    author: FreeCodeCamp
    url: https://www.freecodecamp.org/news/cors-error-how-to-fix/
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src4
    title: "SuperTokens — CORS Errors"
    author: SuperTokens
    url: https://supertokens.com/blog/cors-errors
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src5
    title: "Descope — CORS Errors"
    author: Descope
    url: https://www.descope.com/learn/post/cors
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src6
    title: "Fetch Living Standard — CORS protocol"
    author: WHATWG
    url: https://fetch.spec.whatwg.org/#http-cors-protocol
    type: rfc_spec
    published: 2025-01-01
    reliability: authoritative
  - id: src7
    title: "Chrome — Private Network Access: introducing preflights"
    author: Google Chrome
    url: https://developer.chrome.com/blog/private-network-access-preflight
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src8
    title: "Acunetix — CORS Security Vulnerabilities"
    author: Acunetix
    url: https://www.acunetix.com/blog/articles/cors-security-vulnerabilities/
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src9
    title: "Chrome — New permission prompt for Local Network Access (Chrome 142)"
    author: Google Chrome
    url: https://developer.chrome.com/blog/local-network-access
    type: official_docs
    published: 2025-09-15
    reliability: authoritative
---

# How to Fix CORS Errors in the Browser

## TL;DR

- **Bottom line**: CORS errors are always a **server-side configuration problem** (or a missing server-side proxy). The browser is working correctly — it's enforcing the Same-Origin Policy. The fix is to add the correct `Access-Control-Allow-Origin` (and related) headers to the **server's response**, not to the browser or client code.
- **Key tool/command**: Open DevTools → Network tab → click the failed request → check Response Headers for `Access-Control-Allow-Origin`. If it's missing or wrong, that's your fix target.
- **Watch out for**: `Access-Control-Allow-Origin: *` cannot be combined with `Access-Control-Allow-Credentials: true`. If you need cookies/auth headers cross-origin, you must echo the specific origin back. Also, the `Authorization` header is never covered by the `*` wildcard in `Access-Control-Allow-Headers` — it must be listed explicitly.
- **Works with**: All modern browsers (Chrome, Firefox, Safari, Edge). Applies to Fetch API, XMLHttpRequest, and any browser-initiated cross-origin request. Chrome 130+ also enforces Private Network Access preflights.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Never combine wildcard origin with credentials**: `Access-Control-Allow-Origin: *` + `Access-Control-Allow-Credentials: true` is rejected by all browsers. Echo the specific requesting origin and add `Vary: Origin`. [src1, src4]
- **CORS headers belong on the server response only**: Adding `Access-Control-Allow-Origin` to a `fetch()` request header does nothing — the browser sets `Origin` automatically and checks response headers. [src1, src2]
- **Error responses must include CORS headers**: If your server returns 4xx/5xx without CORS headers, the browser blocks the error response and you see a generic CORS failure. Use `always` in nginx or ensure middleware runs unconditionally. [src3, src4]
- **Local Network Access (Chrome 142+ / Edge 143+)**: The PNA preflight model has been superseded by a permission-based **Local Network Access** prompt. Public websites accessing localhost or private IPs (10/8, 172.16/12, 192.168/16, loopback) now require explicit user permission AND must call `fetch(url, { targetAddressSpace: 'local' })`. The earlier `Access-Control-Allow-Private-Network: true` preflight remains relevant only for legacy Chrome 130–141. [src7, src9]
- **Authorization header requires explicit listing**: The `*` wildcard in `Access-Control-Allow-Headers` does not cover `Authorization` — you must list it explicitly in every CORS configuration. [src1, src6]
- **Cross-origin cookies need SameSite=None; Secure**: Even with correct CORS headers, cookies won't be sent cross-origin in Chrome 80+ unless they have both attributes set. [src1, src4]

## 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 the 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 header to `Access-Control-Allow-Headers` [src1, src3] |
| 6 | Method blocked | Method not in `Allow-Methods` | Add method to `Access-Control-Allow-Methods` [src1] |
| 7 | Cookie not sent cross-origin | `credentials: 'include'` missing | Add `credentials: 'include'` 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 (Chrome 130–141) | 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] |
| 14 | Local Network Access permission denied (Chrome 142+) | LNA permission not granted by user OR `targetAddressSpace` missing | Call `fetch(url, { targetAddressSpace: 'local' })` from HTTPS context; instruct user to grant the permission prompt [src9] |

## Decision Tree

```
START — Browser shows CORS error
├── Open DevTools → Network tab → find the failed request
│   ├── Is there a preflight OPTIONS request?
│   │   ├── YES — OPTIONS request failed (4xx or no CORS headers)
│   │   │   ├── Server returns 404/405 on OPTIONS → add OPTIONS handler [src1, src6]
│   │   │   ├── "No Access-Control-Allow-Private-Network" → add PNA header [src7]
│   │   │   └── OPTIONS succeeds but actual request fails → check Allow-Methods/Headers [src1]
│   │   └── NO — Simple request (GET/POST with simple headers)
│   │       └── Check Response Headers on the failed request
│   │           ├── No Access-Control-Allow-Origin → add it to server [src1, src3]
│   │           ├── Wrong origin value → fix allowlist [src1, src4]
│   │           └── Has * but request has credentials → echo specific origin [src1, src4]
├── Does the request include cookies or Authorization header?
│   ├── YES → Need: specific origin (not *) + Allow-Credentials: true + explicit Authorization in Allow-Headers [src1, src4]
│   └── NO → Wildcard * is fine for public APIs [src1, src5]
├── Is the blocked header a custom one (e.g., X-API-Key, Authorization)?
│   └── YES → Add to Access-Control-Allow-Headers (Authorization always needs explicit listing) [src1, src3, src6]
├── Is the request from a public site to localhost/private IP?
│   └── YES → Add Access-Control-Allow-Private-Network: true [src7]
└── Development only? → Use a dev proxy (Vite/webpack/nginx) [src3, src5]
```

## Step-by-Step Guide

### 1. Diagnose the exact error

Read the browser console and Network tab carefully — the error message tells you exactly what's missing. [src1, src3]

```
# Common browser error messages and what they mean:

"Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present"
→ Server doesn't send the header at all

"has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a
value 'https://other.com' that is not equal to the supplied origin"
→ Server sends wrong origin

"has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present"
→ Server doesn't handle OPTIONS preflight

"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'"
→ Wildcard + credentials conflict

"Request had a target IP address space of private ... No 'Access-Control-Allow-Private-Network'
header was present in the preflight response"
→ Private Network Access preflight missing [src7]
```

**Steps**:
1. Open DevTools (F12) → Console tab → read the full CORS error
2. Go to Network tab → find the failed request (often red)
3. Click it → Response Headers → look for `Access-Control-Allow-Origin`
4. If there's a preflight, find the OPTIONS request above it

### 2. Fix on the server — Express.js (Node.js)

The `cors` npm package is the standard solution for Express. [src1, src3, src4]

```javascript
// Input:  Express app with CORS issues
// Output: Properly configured CORS middleware

const express = require('express');
const cors = require('cors');
const app = express();

// Option A: Allow specific origins (recommended for production)
const allowedOrigins = [
  'https://app.example.com',
  'https://www.example.com',
  // Add staging/preview URLs as needed
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (curl, Postman, server-to-server)
    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,          // Required if client sends cookies/auth headers
  maxAge: 86400,              // Cache preflight for 24 hours
}));

// IMPORTANT: Handle OPTIONS preflight explicitly for all routes
app.options('*', cors());

// Option B: Public API — allow all origins (no credentials)
// app.use(cors());  // Defaults to Access-Control-Allow-Origin: *

app.get('/api/data', (req, res) => {
  res.json({ data: 'hello' });
});
```

### 3. Fix on the server — nginx

Configure CORS headers in nginx for any backend. [src1, src3]

```nginx
# Input:  nginx reverse proxy without CORS headers
# Output: nginx config with proper CORS support

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # Capture the Origin header
        set $cors_origin "";
        if ($http_origin ~* "^https://(app|www)\.example\.com$") {
            set $cors_origin $http_origin;
        }

        # Always add CORS headers (even on error responses)
        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;  # Critical for caching correctness

        # Handle preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' $cors_origin;
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
            add_header 'Access-Control-Max-Age' '86400';
            add_header 'Content-Length' '0';
            add_header 'Content-Type' 'text/plain charset=UTF-8';
            return 204;
        }

        proxy_pass http://backend:8080;
    }
}
```

### 4. Fix on the server — Python (FastAPI / Flask)

> Full script: [4-fix-on-the-server-python-fastapi-flask.py](scripts/4-fix-on-the-server-python-fastapi-flask.py) (44 lines)

```python
# Input:  FastAPI or Flask app with CORS issues
# Output: Properly configured CORS middleware
# === FastAPI ===
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
# ... (see full script)
```

### 5. Development proxy (Vite / webpack)

Never disable browser security. Use a dev proxy instead. [src3, src5]

```javascript
// Input:  Vite or webpack dev server hitting CORS-blocked API
// Output: Dev proxy that bypasses CORS for local development only

// === vite.config.js ===
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,   // Sets Host header to target
        rewrite: (path) => path.replace(/^\/api/, ''),
        secure: true,
      },
    },
  },
};
// Client calls: fetch('/api/data')  → proxied to https://api.example.com/data

// === webpack.config.js (devServer) ===
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' },
      },
    },
  },
};
```

### 6. Client-side: sending credentials correctly

```javascript
// Input:  fetch() call that needs to send cookies or Authorization
// Output: Properly configured fetch with credentials

// Default — credentials NOT sent cross-origin
fetch('https://api.example.com/data');

// Include cookies and auth headers cross-origin
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include',   // Sends cookies; requires server Allow-Credentials: true
  headers: {
    'Authorization': 'Bearer token123',
    'Content-Type': 'application/json',
  },
});

// axios equivalent
import axios from 'axios';
axios.defaults.withCredentials = true;
// Or per-request:
axios.get('https://api.example.com/data', { withCredentials: true });
```

### 7. Handle Private Network Access / Local Network Access (Chrome 130–141 PNA, Chrome 142+ LNA)

If a public website needs to access localhost or private IP addresses, the rules differ by browser version. Chrome 130–141 enforce the PNA preflight model (`Access-Control-Allow-Private-Network: true`). Chrome 142+ and Edge 143+ replace that with a permission prompt: the client must call `fetch(url, { targetAddressSpace: 'local' })` from an HTTPS page, and the user must grant Local Network Access permission. Implement both paths until you have dropped Chrome <142 support. [src7, src9]

```javascript
// Input:  Express server on localhost receiving requests from public website
// Output: Server that handles Private Network Access preflights

const express = require('express');
const app = express();

// Middleware to handle Private Network Access preflight (Chrome 130–141)
app.use((req, res, next) => {
  // Standard CORS headers
  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');

  // Private Network Access header (Chrome 130–141, still relevant for stale browsers)
  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();
});

// === Client-side (Chrome 142+ / Edge 143+) Local Network Access ===
// The page MUST be served over HTTPS. The browser will surface a permission
// prompt the first time the user triggers a local-network request. [src9]
const response = await fetch('http://192.168.1.10/api/status', {
  targetAddressSpace: 'local',  // required from Chrome 142+
});
```

## Anti-Patterns

### Wrong: Wildcard with credentials

```javascript
// BAD — browser blocks this combination [src1, src4]
// Server response:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// Client:
fetch(url, { credentials: 'include' });
// Error: "The value of 'Access-Control-Allow-Origin' must not be '*'
// when credentials mode is 'include'"
```

### Correct: Echo specific origin with credentials

```javascript
// GOOD — reflect the specific requesting origin [src1, src4]
// Server (Express):
app.use(cors({
  origin: (origin, cb) => cb(null, allowedOrigins.includes(origin)),
  credentials: true,
}));
// Server response headers:
// Access-Control-Allow-Origin: https://app.example.com  (the actual origin)
// Access-Control-Allow-Credentials: true
// Vary: Origin  ← critical for CDN/proxy caching
```

### Wrong: Disabling browser security for development

```bash
# BAD — dangerous, masks real issues, never commit this [src3, src5]
# macOS
open -n -a /Applications/Google\ Chrome.app --args \
  --user-data-dir="/tmp/chrome_dev" --disable-web-security

# Windows
chrome.exe --disable-web-security --user-data-dir="%TEMP%\chrome_dev"
```

### Correct: Use a dev proxy

```javascript
// GOOD — proper dev proxy in vite.config.js [src3, src5]
export default {
  server: {
    proxy: {
      '/api': { target: 'https://api.example.com', changeOrigin: true }
    }
  }
};
```

### Wrong: Adding CORS headers on the client

```javascript
// BAD — CORS headers are response headers; client can't set them [src1, src2]
fetch(url, {
  headers: {
    'Access-Control-Allow-Origin': '*',  // This does nothing!
    'Origin': 'https://myapp.com',       // Browser sets this automatically
  }
});
```

### Correct: Fix the server response headers

```javascript
// GOOD — CORS is always fixed on the server [src1, src2]
// In your Express/FastAPI/nginx config, add:
// Access-Control-Allow-Origin: https://myapp.com
// (or use the cors middleware)
```

### Wrong: Using fetch mode 'no-cors' to silence errors

```javascript
// 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'
// response.status === 0
// You cannot read the body, headers, or status
```

### Correct: Fix CORS on the server or use a proxy

```javascript
// GOOD — fix the server, or use a server-side proxy [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

- **CORS is enforced by the browser, not the server**: The server processes the request and returns a response. The browser then checks the CORS headers and decides whether to expose the response to JavaScript. Postman/curl always work because they don't enforce CORS. [src1, src2]
- **Missing `Vary: Origin` header**: If your server echoes the requesting origin dynamically, you must add `Vary: Origin` to prevent CDNs/proxies from caching a response for one origin and serving it to another. [src1, src6]
- **Preflight caching**: `Access-Control-Max-Age` caches preflight results. During development, set it to 0 or 1 to avoid stale preflight responses. In production, 86400 (24h) reduces OPTIONS round-trips. Chrome caps this at 7200 (2h). [src1, src6]
- **Error responses missing CORS headers**: If your server returns a 4xx/5xx error, it must still include CORS headers. Otherwise the browser blocks the error response and you see a generic CORS error instead of the real error. Use `always` in nginx or ensure middleware runs before error handlers. [src3, src4]
- **Trailing slash / port mismatch**: `https://example.com` ≠ `https://example.com/` ≠ `https://example.com:443`. The origin comparison is exact. [src2, src3]
- **Cookies require `SameSite=None; Secure`**: For cross-origin cookies to be sent, they must have `SameSite=None; Secure` attributes (Chrome 80+). Without this, `credentials: 'include'` sends no cookies. [src1, src4]
- **Private Network Access surprises in development**: Chrome 130+ sends PNA preflights when a public website (or even localhost on a different port) accesses a private IP. If your local dev server doesn't handle the `Access-Control-Request-Private-Network` header, requests silently fail. [src7]
- **Authorization header and the wildcard**: Using `Access-Control-Allow-Headers: *` does not cover the `Authorization` header per the Fetch spec. Always list `Authorization` explicitly. [src1, src6]

## Diagnostic Commands

```bash
# === 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"

# 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 ===
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 response headers ===
curl -I -H "Origin: https://app.example.com" https://api.example.com/data

# === Browser DevTools ===
# 1. F12 → Network tab
# 2. Find the failed request (red)
# 3. Click → Headers tab → Response Headers
# 4. Look for: Access-Control-Allow-Origin, Access-Control-Allow-Credentials
# 5. Find the OPTIONS preflight (if any) above the main request

# === Test with fetch in browser console ===
fetch('https://api.example.com/data', {
  credentials: 'include',
  headers: { 'Authorization': 'Bearer test' }
}).then(r => console.log(r.headers.get('Access-Control-Allow-Origin')))
  .catch(e => console.error(e));
```

## Server Configuration Quick Reference

> Full script: [server-configuration-quick-reference.txt](scripts/server-configuration-quick-reference.txt) (36 lines)

```
# Express (Node.js)
npm install cors
app.use(cors({ origin: 'https://app.example.com', credentials: true }))
# FastAPI (Python)
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, ...)
# ... (see full script)
```

## 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 142+ / Edge 143+ (2025-09) | Local Network Access permission prompt | Permission-based replacement for PNA preflight. Requires `fetch(url, { targetAddressSpace: 'local' })` from HTTPS context plus a user-granted permission [src9] |
| Chrome 130+ (2024-10) | Private Network Access enforced (legacy) | Public-to-private requests required `Access-Control-Allow-Private-Network: true` preflight. Superseded by LNA in Chrome 142+ [src7, src9] |
| 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] |
| CORS spec (RFC 6454) | 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 is public and needs cross-origin access | Same-origin requests | No CORS config needed |
| Need cookies/auth cross-origin | Using a 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

- **CORS is not a security mechanism for your API**: It only controls browser access. Anyone can call your API from curl, Postman, or a server. Use authentication (JWT, API keys, sessions) to actually secure your API. [src8]
- **`Access-Control-Allow-Origin: *` is safe for truly public APIs** (e.g., public weather data, open datasets) but must never be used with `credentials: true` or for APIs that return user-specific data. [src1, src8]
- **Preflight requests count against your rate limits**: Each non-simple cross-origin request triggers an OPTIONS preflight. Use `Access-Control-Max-Age` to cache preflight results and reduce overhead. [src1, src6]
- **CORS errors in production often mean a missing header on error responses**: Middleware that only adds CORS headers on success responses will cause confusing CORS errors when the backend returns 4xx/5xx. Always use `always` in nginx or ensure middleware runs unconditionally. [src3, src4]
- **WebSockets don't use CORS**: WebSocket connections use the `Origin` header but are not subject to CORS. They have their own security model. [src1]
- **`fetch()` mode `'no-cors'` is a trap**: It silences the CORS error but gives you an opaque response you can't read. It's only useful for fire-and-forget requests (e.g., analytics pings). [src1, src6]
- **Local Network Access supersedes PNA (Chrome 142+ / Edge 143+, 2025-09)**: Chrome shifted from a CORS-style preflight (`Access-Control-Allow-Private-Network: true`) to a permission-based model. Public sites accessing private IPs (10/8, 172.16/12, 192.168/16, loopback) must now: (1) be served over HTTPS, (2) call `fetch(url, { targetAddressSpace: 'local' })`, and (3) prompt the user to grant Local Network Access permission. Old PNA enforcement (Chrome 130–141) still applies on stale browsers. This also covers WebSocket handshakes. [src7, src9]

## Related Units

- [SSL/TLS Certificate Errors](/software/debugging/ssl-tls-certificate-errors/2026)
- [Kubernetes CrashLoopBackOff](/software/debugging/kubernetes-crashloopbackoff/2026)
- [Node.js ECONNREFUSED](/software/debugging/nodejs-econnrefused/2026)
