Introduction
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls how web pages from one origin can request resources from a different origin. When a frontend at https://app.example.com tries to fetch data from https://api.example.org, the browser enforces a same-origin policy by default. CORS provides a controlled way to relax this policy through HTTP headers. This article explains the complete CORS flow, preflight requests, and how to fix common access-block errors.
Same-Origin Policy
An origin is defined by the combination of scheme, host, and port:
| URL | Origin |
|---|---|
https://example.com/page1 | https://example.com |
https://example.com/page2 | Same origin |
http://example.com/page1 | Different origin (different scheme) |
https://api.example.com | Different origin (different host) |
https://example.com:3000 | Different origin (different port) |
The same-origin policy blocks reading cross-origin responses by default. This prevents malicious sites from stealing data from other origins.
Simple Requests vs Preflight Requests
CORS distinguishes between simple requests and requests that trigger a preflight.
A request is simple if it meets all these conditions:
- Method:
GET,HEAD, orPOST - Content-Type:
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No custom headers
For simple requests, the browser sends the request directly and checks the response for CORS headers.
All other requests trigger a preflight — an OPTIONS request sent before the actual request:
OPTIONS /api/data HTTP/1.1
Host: api.example.org
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
The server must respond with appropriate CORS headers for the browser to proceed.
Core CORS Response Headers
The server controls CORS behavior through these response headers:
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Specifies which origins are permitted |
Access-Control-Allow-Methods | Lists allowed HTTP methods |
Access-Control-Allow-Headers | Lists allowed custom request headers |
Access-Control-Allow-Credentials | Whether credentials (cookies) are allowed |
Access-Control-Max-Age | How long preflight results can be cached |
Access-Control-Expose-Headers | Which response headers are accessible |
Example CORS response for a preflight:
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
Wildcard vs Specific Origins
Using Access-Control-Allow-Origin: * allows any origin but has restrictions:
- Cannot be used with
Access-Control-Allow-Credentials: true - Should only be used for public, read-only APIs
For credentialed requests, you must specify exact origins:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
If you need multiple allowed origins, the server must dynamically echo the requesting origin after validating it against a whitelist.
Handling Cookies with CORS
To include cookies in cross-origin requests, both client and server must opt in:
Client (JavaScript):
fetch('https://api.example.org/data', {
credentials: 'include', // sends cookies
});
Server (response headers):
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Without credentials: 'include', cookies are never sent cross-origin.
Common CORS Errors and Solutions
| Error Message | Likely Cause | Fix |
|---|---|---|
No 'Access-Control-Allow-Origin' header present | Missing CORS headers on server | Add Access-Control-Allow-Origin |
CORS header 'Access-Control-Allow-Origin' mismatch | Origin not in allowed list | Validate origin against whitelist |
Credentials flag is 'true', but the header is '*' | Wildcard with credentials | Use specific origin instead of * |
Method PUT is not allowed by Access-Control-Allow-Methods | Method not listed | Add PUT to allowed methods |
Request header field X-Custom is not allowed | Custom header not in Allow-Headers | Add header to Access-Control-Allow-Headers |
Server Configuration Examples
Express.js:
const cors = require('cors');
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}));
Nginx:
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type";
Conclusion
CORS is a critical security layer that protects users while enabling legitimate cross-origin interactions. Understanding the difference between simple and preflight requests, knowing which headers to configure, and correctly handling credentials are essential skills for full-stack developers. By properly configuring your server’s CORS policy, you can maintain security without blocking valid cross-origin traffic.
