CORS Cheat Sheet
Explains Cross-Origin Resource Sharing headers, preflight requests, server configuration, and common gotchas like credentials plus wildcard.
Key CORS Headers
The response headers that control cross-origin access.
- Access-Control-Allow-Origin- Which origin(s) may access the response; '*' or a specific origin
- Access-Control-Allow-Methods- HTTP methods allowed for cross-origin requests
- Access-Control-Allow-Headers- Custom request headers the client is permitted to send
- Access-Control-Allow-Credentials- true if cookies/auth headers may be included; requires a specific origin
- Access-Control-Max-Age- How long (seconds) the browser may cache a preflight response
- Access-Control-Expose-Headers- Response headers beyond the safelisted set that client JS may read
Enabling CORS (Express)
Using the cors middleware, and the manual equivalent.
const express = require('express');const cors = require('cors');const app = express();// Allow a specific origin with credentialsapp.use(cors({ origin: 'https://app.example.com', methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true,}));// Manual version (no library)app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE'); res.setHeader('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') return res.sendStatus(204); next();});
Preflight Request Example
The automatic OPTIONS handshake before a non-simple request.
# Browser sends this automatically before a "non-simple" request# (e.g. Content-Type: application/json, or custom headers)OPTIONS /api/orders HTTP/1.1Origin: https://app.example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: content-type, authorization# Server must respond with matching allowancesHTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: POSTAccess-Control-Allow-Headers: content-type, authorizationAccess-Control-Max-Age: 86400
Common Gotchas
The mistakes that cause most CORS debugging sessions.
- Wildcard + credentials- Access-Control-Allow-Origin: * cannot be combined with Allow-Credentials: true
- Enforced by the browser- CORS doesn't protect your server; server-to-server requests ignore it entirely
- Simple vs preflighted- GET/POST with only safelisted headers/content-types skip the OPTIONS preflight
- Credentials need exact origin- Can't use a wildcard when cookies/Authorization headers are sent cross-origin
- Preflight caching- Max-Age caches the preflight, but browsers cap the effective duration
Dynamic Origin Whitelist
Validate the request Origin against a whitelist instead of hardcoding a single allowed origin.
const allowedOrigins = new Set([ 'https://app.example.com', 'https://admin.example.com',]);app.use(cors({ origin(origin, callback) { // origin is undefined for same-origin/curl requests if (!origin || allowedOrigins.has(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true,}));// IMPORTANT: reflecting a dynamic origin means the response is NOT// cacheable across origins — always pair with a Vary: Origin header// (the cors middleware sets this automatically).
CORS at the Nginx Layer
Terminating CORS in the reverse proxy so the app server stays origin-agnostic.
location /api/ { set $cors_origin ""; if ($http_origin ~* "^https://(app|admin)\.example\.com$") { set $cors_origin $http_origin; } add_header 'Access-Control-Allow-Origin' $cors_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Vary' 'Origin' always; if ($request_method = OPTIONS) { 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-Max-Age' 86400 always; add_header 'Content-Length' 0; return 204; } proxy_pass http://backend_upstream;}
fetch() mode & credentials
The client-side knobs that determine whether a cross-origin request even attempts to send cookies.
// mode: 'cors' (default for cross-origin) enforces CORS checks on the response// mode: 'no-cors' sends the request but yields an opaque response you can't read// mode: 'same-origin' fails immediately if the URL isn't same-originfetch('https://api.example.com/orders', { mode: 'cors', credentials: 'include', // send cookies even cross-origin (needs ACAC: true server-side) headers: { 'Content-Type': 'application/json' },});// credentials: 'same-origin' (default) never sends cookies cross-origin// credentials: 'omit' never sends cookies even same-origin// A 'no-cors' request to a JSON API is a common mistake: it "succeeds"// but response.type === 'opaque' and status/body are unreadable.
Advanced CORS Concepts
Lesser-known mechanisms that come up once you go past basic preflight setup.
- Vary: Origin- Required whenever the server reflects a dynamic origin, so CDNs/proxies don't cache one origin's response for another
- Private Network Access- Chrome adds an Access-Control-Request-Private-Network preflight when a public page calls localhost/private IPs
- Opaque responses- mode: 'no-cors' responses have unreadable status/headers/body — used for side-effect-only cross-origin calls
- CORP / COEP- Cross-Origin-Resource-Policy and Cross-Origin-Embedder-Policy are separate from CORS but also gate cross-origin loads
- Credentialed subresources- <img>/<script> tags with crossorigin='use-credentials' send cookies and are still subject to CORS checks
- Wildcard subdomains aren't a thing- Access-Control-Allow-Origin has no subdomain wildcard syntax; you must reflect each exact origin
- Redirects and preflight- A cross-origin redirect on a preflighted request fails in most browsers; the target must also handle CORS directly
Sidestepping CORS in Local Dev
Proxying API calls through the dev server so the browser sees only same-origin requests.
// vite.config.jsexport default { server: { proxy: { '/api': { target: 'http://localhost:4000', changeOrigin: true, secure: false, }, }, },};// The browser now calls same-origin '/api/...'; Vite's dev server// forwards it server-to-server, where CORS doesn't apply at all.
CORS errors show up in the browser console but are a client-side restriction only — if the same request works fine via curl or Postman, the fix is missing or incorrect CORS headers on the server, not a network or auth bug.