CORS: Understand and Solve It Once and for All
What is CORS, why it causes errors, how to solve it. Definitive guide to cross-origin errors.
Understanding CORS
CORS (Cross-Origin Resource Sharing) is a security mechanism that restricts web pages from making requests to a different domain than the one serving the page. While it protects users, it often frustrates developers with cryptic error messages. This guide explains how CORS works and provides solutions for common scenarios.
Why CORS Exists
The Same-Origin Policy
Browsers enforce the Same-Origin Policy to prevent malicious sites from accessing data on other sites where a user might be logged in:
// Same origin (allowed)
https://example.com → https://example.com/api ✓
// Different origin (blocked by default)
https://example.com → https://api.example.com ✗
https://example.com → http://example.com ✗
https://example.com → https://example.com:8080 ✗
What Makes an Origin
An origin consists of three parts:
- Protocol: http or https
- Domain: example.com, api.example.com
- Port: 80, 443, 3000, etc.
If any of these differ, it's a different origin.
Common CORS Errors
Typical Error Message
Access to fetch at 'https://api.example.com/data' from origin
'https://example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Preflight Request Failed
Access to fetch at 'https://api.example.com/data' from origin
'https://example.com' has been blocked by CORS policy: Response
to preflight request doesn't pass access control check.
How CORS Works
Simple Requests
Requests that don't trigger a preflight:
- Methods: GET, HEAD, POST
- Standard headers only (Accept, Content-Type with simple values)
- Content-Type: text/plain, multipart/form-data, or application/x-www-form-urlencoded
// Simple request flow
Client Server
| |
|-- GET /api/data -------------->|
| |
|<-- Response -------------------|
| Access-Control-Allow-Origin: *
Preflight Requests
Complex requests trigger an OPTIONS preflight:
// Preflight request flow
Client Server
| |
|-- OPTIONS /api/data ---------->|
| Access-Control-Request-Method: PUT
| Access-Control-Request-Headers: Content-Type
| |
|<-- 204 No Content -------------|
| Access-Control-Allow-Origin: *
| Access-Control-Allow-Methods: PUT
| Access-Control-Allow-Headers: Content-Type
| |
|-- PUT /api/data -------------->|
| Content-Type: application/json
| |
|<-- Response -------------------|
Server-Side Solutions
Express.js
import cors from 'cors';
// Allow all origins (development only!)
app.use(cors());
// Production configuration
app.use(cors({
origin: ['https://example.com', 'https://www.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Allow cookies
maxAge: 86400 // Cache preflight for 24 hours
}));
// Dynamic origin
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://example.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
Manual Headers
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
});
Next.js API Routes
// pages/api/data.js or app/api/data/route.js
export async function GET(request) {
const response = NextResponse.json({ data: 'hello' });
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST');
return response;
}
// For OPTIONS preflight
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
Nginx
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
proxy_pass http://backend;
}
Client-Side Considerations
Credentials (Cookies)
// Include cookies in requests
fetch('https://api.example.com/data', {
credentials: 'include' // Required for cookies
});
// Server must respond with:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com // Cannot be *
Custom Headers
// Custom headers trigger preflight
fetch('https://api.example.com/data', {
headers: {
'Authorization': 'Bearer token',
'X-Custom-Header': 'value'
}
});
// Server must allow these headers:
Access-Control-Allow-Headers: Authorization, X-Custom-Header
Development Workarounds
Proxy in Development
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
// Now requests to /api go through proxy (no CORS)
fetch('/api/data');
Next.js Rewrites
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*'
}
];
}
};
Common Mistakes
Using Wildcard with Credentials
// This won't work!
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// Fix: specify exact origin
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Missing Preflight Handler
// OPTIONS requests return 404
// Fix: handle OPTIONS method
app.options('/api/*', cors()); // Preflight for all routes
Forgetting Allowed Headers
// Error: Request header field authorization is not allowed
// Fix: add to allowed headers
Access-Control-Allow-Headers: Content-Type, Authorization
Debugging CORS
Browser DevTools
- Open Network tab
- Find the failed request
- Check Response Headers for CORS headers
- Look for preflight OPTIONS request
Useful Headers to Check
// Request headers
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type
// Response headers (should be present)
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Content-Type
Tools and Resources
For debugging and development:
- JSON Formatter - Inspect API responses
- Base64 Encoder - Debug auth headers
- URL Encoder - Encode request parameters
Conclusion
CORS errors are frustrating but solvable. Key takeaways:
- CORS is a browser security feature, not a server bug
- Configure CORS on the server, not the client
- Be specific about allowed origins in production
- Use proxies during development to avoid CORS entirely
- Check for preflight (OPTIONS) handling
For more developer resources, explore our free online tools. For the full specification, see MDN CORS documentation.