CORS Deep Dive
Cross-Origin Resource Sharing is the most commonly misunderstood web security mechanism. Developers encounter CORS errors, add Access-Control-Allow-Origin: * to their server, and call it done. Senior engineers understand why CORS exists, how the preflight negotiation works, and when * is dangerously wrong.
The Same-Origin Policy
Two URLs have the same origin if they share the same scheme, host, and port:
https://app.example.com:443/page â origin
â â â
scheme host port
Same origin: https://app.example.com/other-page
Different origin: http://app.example.com (different scheme)
Different origin: https://api.example.com (different host)
Different origin: https://app.example.com:8080 (different port)Without the same-origin policy, any page could read your authenticated data from other sites. CORS is the controlled relaxation of this restriction.
Simple Requests vs Preflight
Simple Requests (No Preflight)
A request skips preflight if it meets all conditions:
- Method:
GET,HEAD, orPOST - Headers: Only safe-listed headers (
Accept,Accept-Language,Content-Language,Content-Typewith specific values) - Content-Type: Only
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
Browser â Server: GET /api/data
Origin: https://app.example.com
Server â Browser: 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{ data: ... }The browser makes the request directly. If the response lacks the correct Access-Control-Allow-Origin, the browser blocks JavaScript from reading the response â the request still reached the server.
Preflight Requests
Any request that doesn't qualify as "simple" triggers a preflight:
Browser â Server: OPTIONS /api/data
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Server â Browser: 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
[If preflight succeeds]
Browser â Server: PUT /api/data
Origin: https://app.example.com
Authorization: Bearer token123
Content-Type: application/jsonCommon triggers for preflight:
Content-Type: application/json- Custom headers (
Authorization,X-Request-ID) - Methods other than GET/HEAD/POST
ReadableStreambody
Preflight Caching
Access-Control-Max-Age tells the browser how long (in seconds) to cache the preflight response:
Access-Control-Max-Age: 86400 // Cache for 24 hoursWithout this, every non-simple request generates two HTTP requests.
Credentials and Cookies
By default, cross-origin requests don't include cookies. To send credentials:
Client:
fetch('https://api.example.com/data', {
credentials: 'include',
});Server must respond with:
Access-Control-Allow-Origin: https://app.example.com (NOT *)
Access-Control-Allow-Credentials: trueWhen credentials are involved, Access-Control-Allow-Origin: * is forbidden. The server must echo the specific origin. This prevents credential leakage to arbitrary origins.
CORS Headers Reference
Response Headers (Server â Browser)
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origins can read the response |
Access-Control-Allow-Methods | Allowed HTTP methods |
Access-Control-Allow-Headers | Allowed request headers |
Access-Control-Expose-Headers | Response headers readable by JS |
Access-Control-Max-Age | Preflight cache duration (seconds) |
Access-Control-Allow-Credentials | Allow cookies/auth headers |
Request Headers (Browser â Server, auto-set)
| Header | Purpose |
|---|---|
Origin | The requesting origin |
Access-Control-Request-Method | Method for actual request (preflight) |
Access-Control-Request-Headers | Headers for actual request (preflight) |
Common CORS Patterns
API Gateway Configuration
const corsOptions = {
origin: (origin, callback) => {
const allowlist = [
'https://app.example.com',
'https://staging.example.com',
];
if (!origin || allowlist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
maxAge: 86400,
};Proxy Pattern (Dev and Production)
// next.config.ts â API proxy to avoid CORS in development
export default {
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' },
];
},
};In production, same-origin API proxies eliminate CORS entirely.
Debugging CORS Issues
- Check the Network tab â Look for the OPTIONS request, verify response headers
- The error is always in the response â CORS errors mean the server's response headers are wrong, not the client's request
- Origin
nullâ Requests fromfile://,data:URIs, or sandboxed iframes sendOrigin: null. Handle explicitly - Redirect after preflight â A redirect on the preflight OPTIONS response will fail. The actual resource URL must respond to OPTIONS directly
Interview Signal
Senior candidates demonstrate:
- Preflight mechanics â When and why OPTIONS fires, what triggers it, caching with Max-Age
- Credentials nuance â
*is invalid with credentials, specific origin echoing requirement - Security understanding â CORS protects the user (browser enforcement), not the server. Server still receives the request
- Debugging fluency â Network tab inspection, understanding that the error is always server-side headers
- Architecture â Same-origin proxy patterns that eliminate CORS complexity entirely