Understanding CORS and How to Handle It
If you've ever worked with APIs, you've almost certainly encountered the dreaded CORS error. That red console message that stops your frontend application dead in its tracks. Yet despite being one of the most common issues developers face, CORS remains widely misunderstood.
This comprehensive guide will demystify Cross-Origin Resource Sharing. You'll learn why browsers enforce this security policy, how the CORS mechanism works under the hood, and most importantly, how to properly configure your servers to handle cross-origin requests.
- What CORS is and why browsers enforce it
- The difference between simple and preflight requests
- Essential CORS headers and what they do
- How to configure CORS in Node.js, Express, Laravel, and Nginx
- Common CORS errors and how to fix them
- Security best practices for CORS configuration
What is CORS?
Cross-Origin Resource Sharing (CORS) is a security mechanism built into web browsers that controls how web applications running on one origin (domain) can request resources from a different origin. It's an extension of the Same-Origin Policy (SOP), which by default blocks cross-origin HTTP requests initiated by scripts.
Origins consist of protocol, domain, and port. Any difference means "cross-origin"
An origin is defined by three components: the protocol (http/https), the domain (example.com), and the port (80, 443, 3000, etc.). If any of these differ between two URLs, they are considered different origins.
Same Origin Examples
https://app.com/page1 β https://app.com/page2https://app.com:443/a β https://app.com/b
Cross Origin Examples
https://app.com β http://app.com (protocol)https://app.com β https://api.app.com (subdomain)
Why Does CORS Exist?
CORS exists to protect users from malicious websites. Without the Same-Origin Policy and CORS, a malicious site could:
- Steal sensitive data β Read your emails from Gmail, view your bank balance, access private APIs
- Perform actions on your behalf β Transfer money, change passwords, post content without consent
- Exploit authenticated sessions β Use your logged-in cookies to access any site you're authenticated to
Imagine you visit evil-site.com while logged into your bank. Without same-origin restrictions, evil-site.com's JavaScript could:
- Fetch your account details from
bank.com/api/balance - Initiate a transfer to the attacker's account
- All using YOUR authenticated session cookies
CORS is the controlled relaxation of this security policy. It allows servers to explicitly declare which origins are permitted to access their resources, enabling legitimate cross-origin communication while maintaining security.
How CORS Works
When a browser makes a cross-origin request, it checks the response headers from the server to determine if the request should be allowed. The server indicates its CORS policy through specific HTTP response headers.
Simple Requests vs. Preflight Requests
CORS categorizes requests into two types: simple requests that go directly to the server, and preflight requests that require a preliminary OPTIONS check.
Simple requests go directly to the server; preflight requests require an OPTIONS check first
The preflight mechanism protects servers that were built before CORS existed. These legacy servers might execute dangerous operations (DELETE, PUT) without expecting cross-origin requests. The OPTIONS check ensures the server explicitly understands and permits the cross-origin request before it's sent.
Essential CORS Headers
CORS is implemented through a set of HTTP headers. Understanding these headers is crucial for proper configuration.
Response Headers (Server β Browser)
| Header | Description | Example Value |
|---|---|---|
Access-Control-Allow-Origin |
Specifies which origin(s) can access the resource | https://app.com or * |
Access-Control-Allow-Methods |
HTTP methods allowed for cross-origin requests | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers |
Custom headers the client is allowed to send | Content-Type, Authorization |
Access-Control-Allow-Credentials |
Whether cookies/auth headers can be included | true |
Access-Control-Max-Age |
How long preflight results can be cached (seconds) | 86400 |
Access-Control-Expose-Headers |
Headers the browser is allowed to access | X-Request-Id, X-Rate-Limit |
Request Headers (Browser β Server)
| Header | Description | Example Value |
|---|---|---|
Origin |
The origin making the request (sent automatically) | https://app.com |
Access-Control-Request-Method |
The HTTP method of the actual request (preflight only) | DELETE |
Access-Control-Request-Headers |
Custom headers to be sent (preflight only) | Content-Type, Authorization |
The Classic CORS Error
This is the error message that brings developers to search engines:
This error means the server didn't include the Access-Control-Allow-Origin header in its response, or the header didn't match the requesting origin.
CORS is enforced by the browser, not the server. The request actually reaches the server and gets a responseβthe browser just refuses to let JavaScript access it. This is why:
- Server-to-server requests work fine (no browser = no CORS)
- Postman, cURL, and other tools ignore CORS
- The "fix" must be implemented on the server
Implementing CORS
Let's look at how to properly configure CORS in various backend environments.
Manual CORS implementation in vanilla Node.js:
const http = require('http');
const server = http.createServer((req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
// Handle preflight requests
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Your actual route handling
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from API' }));
});
server.listen(3000);
Using the cors middleware (recommended):
const express = require('express');
const cors = require('cors');
const app = express();
// Simple usage - allow all origins (not for production!)
app.use(cors());
// Production-ready configuration
const corsOptions = {
origin: ['https://myapp.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
credentials: true, // Allow cookies
maxAge: 86400, // Cache preflight for 24 hours
exposedHeaders: ['X-Request-Id'] // Headers client can access
};
app.use(cors(corsOptions));
// Dynamic origin based on request
const dynamicCors = cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
// Allow requests with no origin (mobile apps, curl)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
});
app.use(dynamicCors);
Laravel includes CORS middleware since version 7. Configure in config/cors.php:
<?php
return [
/*
* Which paths should handle CORS
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
/*
* Matches request origins against patterns
*/
'allowed_origins' => [
'https://myapp.com',
'https://admin.myapp.com',
],
// Or use patterns
'allowed_origins_patterns' => [
'https://*.myapp.com',
],
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'exposed_headers' => ['X-Request-Id'],
'max_age' => 86400,
'supports_credentials' => true,
];
Make sure the HandleCors middleware is in your app/Http/Kernel.php global middleware stack.
Add CORS headers at the server level in Nginx:
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
proxy_pass http://backend;
}
}
CORS with Credentials
When your API needs to accept cookies or authorization headers, CORS configuration becomes more restrictive.
Server: Enable credentials
Set Access-Control-Allow-Credentials: true in response headers.
Server: Specify exact origin
You cannot use * for Allow-Origin when credentials are enabled. Must specify the exact origin.
Client: Include credentials in request
Set credentials: 'include' in fetch or withCredentials: true in Axios.
// Fetch API
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include', // Include cookies
headers: {
'Content-Type': 'application/json',
}
});
// Axios
axios.get('https://api.example.com/user', {
withCredentials: true // Include cookies
});
// Axios global config
axios.defaults.withCredentials = true;
Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. This is forbidden by the spec because it would allow any website to make authenticated requests to your API using the user's credentials.
Common CORS Errors and Solutions
Error: No 'Access-Control-Allow-Origin' header
Cause: Server isn't sending CORS headers.
Solution: Add the Access-Control-Allow-Origin header to your server responses. Make sure it's added for the specific routes being accessed.
res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com');
Error: Origin not in Access-Control-Allow-Origin
Cause: The requesting origin doesn't match the allowed origin.
Solution: Verify the origin matches exactly (including protocol, domain, and port). Check for trailing slashes or www vs non-www differences.
// Wrong: port mismatch
'https://localhost:3000' !== 'https://localhost:3001'
// Wrong: trailing slash
'https://app.com/' !== 'https://app.com'
// Wrong: www mismatch
'https://www.app.com' !== 'https://app.com'
Error: Request header not allowed
Cause: You're sending a custom header that isn't in Access-Control-Allow-Headers.
Solution: Add the header to your CORS configuration.
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Custom-Header');
Error: Method not allowed
Cause: The HTTP method (PUT, DELETE, etc.) isn't in Access-Control-Allow-Methods.
Solution: Add the method to your allowed methods list.
res.setHeader('Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, PATCH, OPTIONS');
Error: Wildcard (*) with credentials
Cause: Using * for Allow-Origin while also requiring credentials.
Solution: Dynamically set the origin based on the request's Origin header.
const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
Security Best Practices
CORS configuration is a security decision. Here's how to do it right:
CORS Security Checklist
- Never use
*in production β Always specify exact allowed origins - Validate the Origin header β Check against a whitelist, don't blindly echo it back
- Be specific with methods β Only allow the HTTP methods your API actually uses
- Limit allowed headers β Only permit headers your frontend actually sends
- Set appropriate Max-Age β Cache preflight results to reduce OPTIONS requests
- Review credentials setting β Only enable if you need cookies/auth headers
- Don't bypass CORS in production β Proxy solutions should only be for development
- Monitor and log CORS failures β They might indicate an attack or misconfiguration
A dangerous anti-pattern is setting Access-Control-Allow-Origin to whatever the request's Origin header contains. This effectively disables CORS protection entirely. Always validate against a whitelist of allowed origins.
Development Workarounds
During development, you might need to bypass CORS temporarily. Here are safe approaches:
Proxy Server
Configure your dev server (Vite, Webpack, CRA) to proxy API requests. The proxy runs same-origin, so no CORS issues.
Dev-only CORS
Enable permissive CORS only in development mode. Use environment variables to control the configuration.
Browser Extensions
Extensions like "CORS Unblock" disable CORS for testing. Never use in production browsing!
Chrome Flags
Launch Chrome with --disable-web-security. Only for isolated testing environments.
export default {
server: {
proxy: {
// Proxy /api requests to backend
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
Debugging CORS Issues
When troubleshooting CORS, your browser's developer tools are your best friend:
Check the Network Tab
Look at the actual request and response headers. Verify the Origin, Access-Control-Allow-Origin, and other CORS headers are present and correct.
Look for Preflight (OPTIONS)
If your request triggers a preflight, check if the OPTIONS request succeeds and returns proper CORS headers. A failing preflight blocks the actual request.
Test with cURL
Use cURL to verify the server returns correct headers. This eliminates browser-specific issues from troubleshooting.
# Check simple request headers
curl -I -H "Origin: https://myapp.com" \
https://api.example.com/data
# Simulate preflight request
curl -X OPTIONS \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-I https://api.example.com/data
# Look for these in response:
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Methods: POST, GET, ...
# Access-Control-Allow-Headers: Content-Type, Authorization
Key Takeaways
Summary
- CORS is browser-enforced β The server receives and processes the request; the browser decides whether to expose the response to JavaScript
- Same-origin = same protocol + domain + port β Any difference triggers cross-origin policies
- Simple vs preflight β Complex requests trigger an OPTIONS preflight check
- Server must opt-in β CORS headers explicitly permit cross-origin access
- Be specific in production β Never use wildcard (*) with real applications
- Credentials require exact origin β Can't use * when cookies are involved
- Debug with network tools β Check headers, look for preflight failures
Understanding CORS transforms it from a frustrating obstacle into a manageable security feature. With proper server configuration, your APIs can safely serve web applications across different origins while maintaining the security protections that keep users safe.
