Fixing CORS Errors: What They Are and How to Resolve Them
A comprehensive guide to learning how to find, debug, and fix CORS errors in your web applications.
What Is a CORS Error and Why Does It Happen?
CORS errors are one of the most common stumbling blocks in modern web development. Your API works perfectly in Postman, but the moment you try to fetch data from your frontend, the browser blocks the request with a cryptic error message.
Definition of CORS
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls whether JavaScript running on one website can access resources from another website. It extends the Same-Origin Policy, which serves as the browser’s fundamental trust boundary.
An origin consists of three parts:
- Protocol:
http://
vshttps://
- Domain:
example.com
vsapi.example.com
- Port:
:3000
vs:8080
Any difference in these components triggers a cross-origin request, which requires explicit permission from the server.
// Same origin requests
fetch('/api/users') // Relative URL
fetch('https://myapp.com/api/data') // Same protocol, domain, port
// Cross-origin requests
fetch('https://api.myapp.com/users') // Different subdomain
fetch('http://myapp.com/api/users') // Different protocol
fetch('https://myapp.com:8080/api/users') // Different port
The Same-Origin Policy exists to prevent malicious websites from reading sensitive data from other sites. Without it, any website could access your Gmail, initiate bank transfers, or steal session tokens.
What Triggers a CORS Error?
A CORS error occurs when your browser blocks a response because the server didn’t include the required permission headers. Here’s the typical sequence:
- Your frontend at
http://localhost:3000
makes a request tohttps://api.yourservice.com
- The browser detects a cross-origin request
- The request is sent to the server
- The server responds without CORS headers
- The browser blocks JavaScript from accessing the response
- A CORS error appears in the console
The crucial detail: the request often completes successfully on the server. CORS doesn’t prevent the request, it prevents your JavaScript from reading the response. This distinction matters when debugging issues like duplicate database entries despite console errors.
CORS Preflight and Actual Requests
Browsers categorize HTTP requests into two types for CORS purposes:
Simple Requests proceed without preflight:
- GET, HEAD, or POST methods
- Limited headers (Accept, Content-Language, Content-Type with restrictions)
- Content-Type must be
application/x-www-form-urlencoded
,multipart/form-data
, ortext/plain
Preflighted Requests require permission first:
- Any other HTTP method (PUT, DELETE, PATCH)
- Custom headers like
Authorization
orX-API-Key
- POST with
Content-Type: application/json
Here’s the preflight flow:
# Browser sends preflight
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Server responds with permissions
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
# Browser sends actual request
POST /api/users HTTP/1.1
Origin: http://localhost:3000
Authorization: Bearer your-token-here
Content-Type: application/json
{"name": "New User"}
# Server includes CORS headers with response
HTTP/1.1 201 Created
Access-Control-Allow-Origin: http://localhost:3000
{"id": 123, "name": "New User"}
This preflight process explains why JSON API calls appear slower in browsers than in Postman, browsers must complete the permission check first, while Postman skips CORS entirely.
Common Types of CORS Errors and Their Meaning
No “Access-Control-Allow-Origin” header
This error means the server didn’t include the header that authorizes your origin to access the response.
fetch('https://api.coolservice.com/data')
.then(res => res.json())
.then(data => console.log(data))
// Error:
// Access to fetch at 'https://api.coolservice.com/data' from origin
// 'http://localhost:3000' has been blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present on the requested resource.
The server processed your request but didn’t include the required header. Your browser discards the response before JavaScript can access it.
For servers you control:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
next();
});
For external APIs, you’ll need a proxy server or the API must support CORS.
“Method not allowed by Access-Control-Allow-Methods”
The server accepts some HTTP methods but not the one you’re using.
fetch('https://api.example.com/users/123', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer token'
}
})
// Error:
// Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.
This occurs during preflight when the server’s Access-Control-Allow-Methods
header doesn’t include your method. Common causes include missing OPTIONS endpoints or incomplete method lists.
“Credentialed requests not supported”
This error appears when sending cookies or authentication headers cross-origin with incorrect server configuration.
fetch('https://api.example.com/profile', {
credentials: 'include'
})
// Error:
// 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'.
Credentialed requests require:
- Specific origin (not wildcard)
Access-Control-Allow-Credentials: true
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Credentials', 'true');
CORS Errors in Fetch vs. Axios
Fetch and Axios handle CORS differently:
// Fetch - generic error messages
fetch('https://api.example.com/data')
.catch(error => {
console.log(error.message); // "Failed to fetch"
});
// Axios - more detailed error handling
axios.get('https://api.example.com/data')
.catch(error => {
if (error.response) {
console.log('Server responded with error:', error.response.status);
} else if (error.request) {
console.log('No response received:', error.message);
}
});
Key differences:
- Fetch provides generic “Failed to fetch” for network errors including CORS
- Axios automatically sets
Content-Type: application/json
, triggering preflight - Axios interceptors can add headers that unexpectedly trigger preflight
How to Fix CORS Errors (Frontend & Backend)
CORS is fundamentally a server-side configuration. Frontend workarounds exist for development, but production requires proper backend setup.
Set Proper Headers on the Server:
Access-Control-Allow-Origin
// Development
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
// Production with multiple origins
const allowedOrigins = ['https://app.example.com', 'https://beta.example.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
Access-Control-Allow-Methods
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
Access-Control-Allow-Headers
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
Access-Control-Allow-Credentials
res.header('Access-Control-Allow-Credentials', 'true');
Access-Control-Max-Age
res.header('Access-Control-Max-Age', '86400');
Enable CORS in Node.js/Express
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000'
];
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
Configuring CORS in Other Backends
Flask
from flask import Flask, jsonify, request
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=["https://app.example.com"], supports_credentials=True)
@app.after_request
def after_request(response):
origin = request.headers.get('Origin')
if origin in ['https://app.example.com', 'http://localhost:3000']:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
Django
# settings.py
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"https://app.example.com",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept',
'authorization',
'content-type',
'x-csrftoken',
]
Spring Boot
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com", "http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(3600);
}
}
Frontend Considerations
What doesn’t work, adding CORS headers to fetch requests has no effect:
fetch('https://api.example.com/data', {
headers: {
'Access-Control-Allow-Origin': '*',
}
})
The no-cors
mode prevents reading responses:
fetch('https://api.example.com/data', {
mode: 'no-cors'
})
.then(response => response.json())
What actually works, using a proxy during development:
// Vite config
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
Ensure correct request configuration:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
SuperTokens and CORS Configuration
Authentication adds complexity to CORS because credentials require stricter security rules. SuperTokens simplifies most of this, but understanding the underlying mechanics prevents configuration issues.
Why CORS Matters for Auth Flows
SuperTokens uses httpOnly cookies for session management, which immediately impacts CORS requirements:
fetch('https://api.example.com/auth/session/verify', {
method: 'GET',
credentials: 'include'
})
With credentials, CORS rules become stricter:
- No wildcard origins allowed
Access-Control-Allow-Credentials
must betrue
- Origins must match exactly
Authentication Headers Trigger Preflight
fetch('https://api.example.com/auth/signin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'rid': 'emailpassword',
'st-auth-mode': 'cookie'
},
credentials: 'include',
body: JSON.stringify({ email, password })
})
How SuperTokens Handles CORS
SuperTokens automatically manages CORS for its authentication endpoints:
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
supertokens.init({
framework: "express",
supertokens: {
connectionURI: "https://try.supertokens.com",
},
appInfo: {
appName: "MyApp",
apiDomain: "https://api.example.com",
websiteDomain: "https://app.example.com",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
Session.init({
cookieSameSite: "none",
cookieSecure: true,
})
]
});
SuperTokens derives CORS settings from appInfo
, but only for its own routes. Your API endpoints need separate configuration.
Common CORS Pitfalls with SuperTokens
Mismatched Domains:
supertokens.init({
appInfo: {
apiDomain: "http://localhost:8080",
websiteDomain: "http://localhost:3000"
}
});
Cookie Configuration:
Session.init({
cookieSameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
cookieSecure: process.env.NODE_ENV === "production"
})
API Route Configuration:
app.use(cors({
origin: ["https://app.example.com", "http://localhost:3000"],
credentials: true
}));
Secure Implementation Example
Express Setup:
import express from 'express';
import cors from 'cors';
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import { middleware, errorHandler } from "supertokens-node/framework/express";
const app = express();
const apiDomain = process.env.API_DOMAIN || "http://localhost:8080";
const websiteDomain = process.env.WEBSITE_DOMAIN || "http://localhost:3000";
const isProduction = process.env.NODE_ENV === "production";
supertokens.init({
framework: "express",
supertokens: {
connectionURI: process.env.SUPERTOKENS_CONNECTION_URI,
apiKey: process.env.SUPERTOKENS_API_KEY,
},
appInfo: {
appName: "MySecureApp",
apiDomain,
websiteDomain,
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
Session.init({
cookieSameSite: isProduction ? "none" : "lax",
cookieSecure: isProduction,
antiCsrf: "VIA_TOKEN",
})
]
});
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [websiteDomain];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'rid', 'st-auth-mode'],
};
app.use(cors(corsOptions));
app.use(middleware());
app.get('/api/user/profile',
Session.verifySession(),
async (req, res) => {
const userId = req.session.getUserId();
res.json({ userId, profile: "..." });
}
);
app.use(errorHandler());
Frontend Configuration:
import SuperTokens from "supertokens-auth-react";
import Session from "supertokens-auth-react/recipe/session";
SuperTokens.init({
appInfo: {
appName: "MyApp",
apiDomain: "http://localhost:8080",
websiteDomain: "http://localhost:3000",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [Session.init()]
});
async function fetchUserData() {
const response = await fetch(`${API_URL}/user/profile`, {
method: 'GET',
credentials: 'include',
headers: { 'Content-Type': 'application/json' }
});
return response.json();
}
Best Practices for Secure CORS Handling
Avoid Wildcard Origins in Production
Using Access-Control-Allow-Origin: *
in production eliminates the security benefits of CORS.
const allowedOrigins = {
production: ['https://app.yourcompany.com', 'https://www.yourcompany.com'],
staging: ['https://staging.yourcompany.com'],
development: ['http://localhost:3000', 'http://localhost:3001']
};
const corsOptions = {
origin: function (origin, callback) {
const env = process.env.NODE_ENV || 'development';
const allowed = allowedOrigins[env];
if (!origin) return callback(null, true);
if (allowed.includes(origin)) {
callback(null, true);
} else {
console.error(`CORS rejected origin: ${origin}`);
callback(new Error('CORS policy violation'));
}
}
};
Exception: truly public, read-only APIs can use wildcards:
app.get('/api/public/weather', cors({ origin: '*' }), (req, res) => {
res.json({ temperature: 72, conditions: 'sunny' });
});
Control Credentials Carefully
Credentials enforce stricter CORS rules:
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization'],
methods: ['GET', 'POST'],
exposedHeaders: ['X-Total-Count']
};
const cookieConfig = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
domain: process.env.NODE_ENV === 'production' ? '.example.com' : undefined
};
Whitelist Only Necessary Origins
class CORSWhitelist {
constructor() {
this.origins = new Map([
['production', new Set([
'https://app.example.com',
'https://www.example.com'
])],
['staging', new Set([
'https://staging.example.com'
])],
['development', new Set([
'http://localhost:3000'
])]
]);
}
isAllowed(origin, environment = process.env.NODE_ENV) {
const allowedForEnv = this.origins.get(environment);
return allowedForEnv ? allowedForEnv.has(origin) : false;
}
}
const whitelist = new CORSWhitelist();
app.use(cors({
origin: (origin, callback) => {
if (!origin || whitelist.isAllowed(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
}
}));
Monitor Preflight Requests
app.options('*', (req, res, next) => {
const preflightData = {
timestamp: new Date().toISOString(),
origin: req.headers.origin || 'no-origin',
method: req.headers['access-control-request-method'],
headers: req.headers['access-control-request-headers'],
path: req.path
};
if (!req.headers.origin) {
console.info('Preflight without origin:', preflightData);
} else if (!whitelist.isAllowed(req.headers.origin)) {
console.warn('Rejected preflight:', preflightData);
}
next();
});
Debugging CORS Issues in Production
Production CORS issues often stem from infrastructure rather than application code. The browser’s Network tab provides crucial debugging information.
Using Browser Developer Tools
The Network tab reveals the complete request/response cycle:
# Healthy CORS flow
OPTIONS /api/users 204 2ms (preflight)
POST /api/users 201 45ms (actual request)
# Failed CORS flow
POST /api/users --- 0ms (CORS error - no preflight)
Key indicators to check:
- Missing OPTIONS requests indicate server preflight handling issues
- Response headers show what CORS headers the server actually sent
- Timing information reveals if responses were received but blocked
Server Log Analysis
# Healthy preflight
[2024-01-15 14:23:45] OPTIONS /api/users 204 2ms
[2024-01-15 14:23:45] POST /api/users 201 45ms
# Common production issues
[2024-01-15 14:23:45] POST /api/users 500 "Method not allowed"
[2024-01-15 14:23:45] OPTIONS /api/users 401 "Unauthorized"
Enhanced logging for debugging:
app.use((req, res, next) => {
if (req.method === 'OPTIONS' || req.headers.origin) {
console.log('[CORS Debug]', {
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
origin: req.headers.origin,
headers: {
'access-control-request-method': req.headers['access-control-request-method'],
'access-control-request-headers': req.headers['access-control-request-headers'],
}
});
}
next();
});
Testing Tools
Test actual browser behavior with curl:
# Simulate preflight
curl -X OPTIONS https://api.production.com/users \
-H "Origin: https://app.production.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
# Look for these headers in response
< access-control-allow-origin: https://app.production.com
< access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS
< access-control-allow-headers: content-type, authorization
Common production-specific issues:
- CDNs stripping CORS headers
- Load balancers blocking OPTIONS
- Reverse proxies changing paths
- Missing environment variables
Summary and Developer Checklist
Core CORS Principles
- CORS is server-configured - Frontend workarounds don’t fix production issues
- Credentials change the rules - No wildcards, explicit origins required
- Infrastructure matters - CDNs, load balancers, and proxies affect CORS
- Preflight is critical - OPTIONS must work for complex requests
Developer CORS Checklist
Server Headers
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400');
Origin Configuration
const ALLOWED_ORIGINS = {
development: ['http://localhost:3000'],
staging: ['https://staging.example.com'],
production: ['https://app.example.com']
};
function getAllowedOrigin(req) {
const origin = req.headers.origin;
const env = process.env.NODE_ENV || 'development';
const allowedOrigins = ALLOWED_ORIGINS[env] || [];
if (!origin) return '*';
if (allowedOrigins.includes(origin)) return origin;
console.warn(`CORS: Rejected origin ${origin}`);
return false;
}
Credential Handling
// Frontend
fetch(url, { credentials: 'include' });
// Backend
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', specificOrigin);
// Cookies
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'none'
});
Preflight Handling
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', getAllowedOrigin(req));
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', req.headers['access-control-request-headers']);
res.header('Access-Control-Max-Age', '86400');
res.sendStatus(204);
});
Production Deployment Verification
#!/bin/bash
# CORS verification script
API_DOMAIN="https://api.example.com"
FRONTEND_DOMAIN="https://app.example.com"
# Test preflight
echo "Testing preflight..."
curl -s -X OPTIONS "$API_DOMAIN/api/test" \
-H "Origin: $FRONTEND_DOMAIN" \
-H "Access-Control-Request-Method: POST" \
-I | grep -i "access-control"
# Test actual request
echo "Testing actual request..."
curl -s -X GET "$API_DOMAIN/api/health" \
-H "Origin: $FRONTEND_DOMAIN" \
-I | grep -i "access-control"
Emergency Production Fix
When production fails and you need immediate resolution:
app.use((req, res, next) => {
console.warn('EMERGENCY CORS MODE - TEMPORARY ONLY');
res.header('Access-Control-Allow-Origin', req.headers.origin || '*');
res.header('Access-Control-Allow-Methods', '*');
res.header('Access-Control-Allow-Headers', '*');
res.header('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Then gradually restrict based on actual requirements.
Final Recommendations
- Test CORS configuration early in development
- Log CORS requests and rejections in production
- Understand your infrastructure’s impact on headers
- Use specific origins instead of wildcards
- Keep authentication flows simple to minimize CORS complexity
With proper understanding and configuration, CORS becomes a powerful security ally rather than a development obstacle.