Table of Contents
What is CORS and why does it exist?
Practical analogy: Think of CORS as an invitation system for an event. Your frontend (the guest) wants to access resources from an API (the event). The browser (the security guard) checks whether the API issued a valid invitation (CORS headers). Without it, access is denied.
CORS (Cross-Origin Resource Sharing) is a security mechanism implemented by all modern browsers that allows web servers to explicitly declare which external origins are allowed to access their resources.
What counts as an “origin”?
An origin is defined by the unique combination of three components:
| Component | Example | Does it change the origin? |
|---|---|---|
| Protocol (Scheme) | http:// vs https:// |
✅ Yes |
| Domain (Host) | api.domain.com vs app.domain.com |
✅ Yes |
| Port | :3000 vs :8080 |
✅ Yes |
Examples of different origins:
// SAME ORIGIN
https://app.mydomain.com:443 → https://app.mydomain.com:443
// DIFFERENT ORIGINS
https://app.mydomain.com → https://api.mydomain.com // Different host
https://app.mydomain.com → http://app.mydomain.com // Different protocol
https://app.mydomain.com → https://app.mydomain.com:8080 // Different port
https://app.mydomain.com → https://mydomain.com // Different subdomain
The “CORS error” is not a server error
Key insight: When you see a CORS error in the browser console, your server already responded (sometimes even with 200 OK). The browser is simply blocking your JavaScript from accessing that response for security reasons.
Typical request flow with CORS:
- Frontend: Your app at
https://app.comcallshttps://api.com/data - Browser: Detects a cross-origin request and adds
Origin: https://app.com - Server: Responds with data but without
Access-Control-Allow-Origin: https://app.com - Browser: Blocks access to the response → CORS error in the console
Why does it work in Postman/curl but not in the browser?
Tools like Postman, curl, or server-side scripts do not enforce the Same-Origin Policy. Only web browsers apply this restriction for security.
Common CORS error causes and diagnosis
1. Missing or incorrect Access-Control-Allow-Origin
The server must include this header with the exact frontend origin, or * (with limitations).
Correct configuration:
// For a specific origin
Access-Control-Allow-Origin: https://app.mydomain.com
// For multiple origins (requires backend logic)
// The server must validate Origin and reply with the appropriate value
2. Preflight request (OPTIONS) failure
The browser sends an OPTIONS request before “non-simple” requests:
Typical OPTIONS request:
OPTIONS /api/users HTTP/1.1
Host: api.mydomain.com
Origin: https://app.mydomain.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, Content-Type
Expected OPTIONS response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.mydomain.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 // Cache for 24 hours
3. Credentials (cookies/auth) misconfiguration
When you send cookies or use credentials: 'include' in fetch:
Incorrect configuration:
// Frontend
fetch('https://api.com/data', {
credentials: 'include' // Sends cookies
});
// Backend (ERROR - not compatible with credentials)
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true // Contradiction
Correct configuration:
// Backend
Access-Control-Allow-Origin: https://app.mydomain.com // Specific, NOT *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Auth-Token // Custom headers visible to JS
4. Custom headers not allowed
Headers like X-API-Key or X-Custom-Header must be explicitly allowed:
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key, X-Custom-Header
Practical solutions by environment and stack
Express.js (Node.js)
const express = require('express');
const cors = require('cors');
const app = express();
// Basic development setup
app.use(cors({
origin: 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
// Production setup (allowlist)
const allowedOrigins = [
'https://app.mydomain.com',
'https://staging.mydomain.com'
];
app.use(cors({
origin: function(origin, callback) {
// Allow requests without Origin (e.g., mobile apps or curl)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) === -1) {
const msg = `Origin ${origin} is not allowed by CORS`;
return callback(new Error(msg), false);
}
return callback(null, true);
},
credentials: true
}));
Django (Python)
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be before CommonMiddleware
...
]
# CORS configuration
CORS_ALLOWED_ORIGINS = [
"https://app.mydomain.com",
"https://staging.mydomain.com",
"http://localhost:3000",
]
# Local development only
CORS_ALLOW_ALL_ORIGINS = True # Development only
# Allow cookies
CORS_ALLOW_CREDENTIALS = True
# Allowed headers
CORS_ALLOW_HEADERS = [
'content-type',
'authorization',
'x-csrftoken',
'x-requested-with',
]
Nginx (reverse proxy) configuration
server {
listen 80;
server_name api.mydomain.com;
location / {
# CORS preflight
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.mydomain.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# Normal requests
add_header 'Access-Control-Allow-Origin' 'https://app.mydomain.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
# Proxy to your app
proxy_pass http://localhost:8080;
}
}
Development approach: Proxy in Vite/Webpack
// vite.config.js (Vite)
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
// Frontend calls /api/users -> Vite forwards to localhost:3001/users
// Same origin from the browser's perspective = fewer CORS problems
CORS and security: what you should know
Important: CORS is NOT an authentication or authorization mechanism. It only controls which frontends can read your API responses from a browser.
Risky myths about CORS
Myth: “If I configure CORS, my API is secure”
CORS only affects browsers. Anyone can call your API directly using curl, Postman, or scripts.
Reality: Security requires multiple layers
- Authentication: JWT, OAuth, sessions
- Authorization: RBAC, resource-level permissions
- Validation: input sanitization, schema validation
- Rate limiting: abuse protection
- CORS: one additional layer
Recommended security patterns
API keys for native/mobile apps
For non-browser clients, use API keys with appropriate, client-specific rate limiting.
Strict CORS in production
Use an allowlist of origins. Do not use * in production when credentials are involved.
Rotating refresh tokens
For SPAs, implement refresh token rotation to reduce the impact of token theft.
Advanced cases and edge cases
WebSockets and CORS
WebSockets are not subject to CORS, but many servers still validate the Origin header manually.
// Socket.io configuration
const io = require('socket.io')(server, {
cors: {
origin: "https://app.mydomain.com",
methods: ["GET", "POST"],
credentials: true
}
});
CDNs and CORS for static assets
For web fonts, images, or other assets hosted on a CDN:
# AWS S3 CORS configuration
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://app.mydomain.com"],
"ExposeHeaders": []
}
]
Microservices and internal CORS
In microservice architectures, consider:
- An API gateway handling CORS once
- Internal service-to-service communication without CORS concerns
- Custom headers for tracing across services
CORS implementation checklist
Extended FAQ
How do I debug CORS issues in the browser?
In DevTools → Network:
- Filter by “OPTIONS” to find preflight requests
- Inspect request and response headers
- Confirm
Access-Control-Allow-Originmatches exactly - Use “Copy as cURL” to test outside the browser
Can I use wildcards for subdomains?
Yes, but with limitations:
// VALID for specific subdomain patterns (implementation-dependent)
Access-Control-Allow-Origin: https://*.mydomain.com
// INVALID - do not mix protocols
Access-Control-Allow-Origin: *://app.mydomain.com
// BEST PRACTICE: Validate Origin dynamically on the backend
const allowedPattern = /^https:\/\/(app|staging|api)\.mydomain\.com$/;
What about redirects (301/302)?
Redirects can drop CORS headers. Typical fixes:
- Configure CORS on the server issuing the redirect
- Avoid redirects in APIs; return appropriate status codes
- In Nginx/Apache, ensure CORS headers are set for all responses
Does CORS affect Server-Side Rendering (SSR)?
No, because SSR runs on the server. However:
- Client-side calls during hydration are still subject to CORS
- Consider API routes in your framework (Next.js, Nuxt) to reduce CORS surface area