CORS Guide
Cross-Origin Resource Sharing explained: same-origin policy, preflight requests, headers, and how to fix common CORS errors.
What is CORS?
Browsers enforce the same-origin policy: a page at https://app.com cannot make JavaScript requests to https://api.other.com by default. An origin is the combination of scheme + host + port.
CORS (Cross-Origin Resource Sharing) is the mechanism that lets servers opt-in to allowing cross-origin requests. The server signals permission through HTTP response headers. Crucially, CORS is enforced by the browser — curl and server-to-server requests are never affected.
Without CORS, a malicious page could silently read your bank data or perform actions on your behalf on any site you're logged into.
How CORS Works
For a simple request (GET/POST with basic headers):
- Browser sends request with an
Origin: https://app.comheader. - Server responds with
Access-Control-Allow-Origin: https://app.com. - Browser checks the header. If it matches, the response is exposed to JavaScript.
For a preflighted request (PUT/DELETE, custom headers, JSON body):
- Browser automatically sends an
OPTIONSrequest first. - Server responds with allowed origins, methods, and headers.
- If approved, browser sends the actual request.
- Server responds normally with CORS headers again.
Key CORS Headers
| Header | Direction | Description |
|---|---|---|
Access-Control-Allow-Origin | Response | Which origin(s) may access the resource. Use * for public or an exact origin for credentialed requests. |
Access-Control-Allow-Methods | Response (preflight) | Comma-separated list of HTTP methods allowed: GET, POST, PUT, DELETE. |
Access-Control-Allow-Headers | Response (preflight) | Which request headers are allowed (e.g., Authorization, Content-Type). |
Access-Control-Allow-Credentials | Response | Set to true to allow cookies and auth headers. Requires explicit origin (not *). |
Access-Control-Max-Age | Response (preflight) | Seconds to cache the preflight result, avoiding repeated OPTIONS requests. Max ~7200 in Chrome, 86400 in Firefox. |
Access-Control-Expose-Headers | Response | Headers beyond the safe list that JS may read (e.g., X-RateLimit-Remaining). |
Origin | Request | The origin of the requesting page, added automatically by the browser. |
Simple vs Preflight Requests
A request is simple (no preflight) only if all of these are true:
- Method is
GET,HEAD, orPOST - Headers are only:
Accept,Accept-Language,Content-Language,Content-Type - Content-Type (if present) is
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No
ReadableStreamin the body, no event listeners on XHR upload
Sending Content-Type: application/json, using Authorization, or using PUT/DELETE will all trigger a preflight.
Common CORS Errors and Fixes
1. Missing Access-Control-Allow-Origin
Error: "No 'Access-Control-Allow-Origin' header is present"
Fix: Add the header to your server response. For public APIs use Access-Control-Allow-Origin: *. For credentialed requests, echo back the exact requesting origin after validating it against an allowlist.
2. Wildcard + Credentials Conflict
Error: "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'"
Fix: Replace * with the specific origin. Also add Access-Control-Allow-Credentials: true.
3. Preflight Not Handled
Error: The OPTIONS request returns 404 or 405.
Fix: Add a route or middleware that responds to OPTIONS with the appropriate CORS headers and a 204 No Content status.
Server-Side Examples
Express.js (cors middleware)
const cors = require('cors');
// Public API — allow all origins
app.use(cors());
// Credentialed API — specific origin only
app.use(cors({
origin: 'https://app.example.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
})); Nginx
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
} Apache (.htaccess)
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L] Debug CORS headers interactively with the CORS Debugger tool.
Frequently Asked Questions
Why do I get a CORS error?
CORS errors happen when your browser makes a request to a different origin (scheme + host + port) and the server's response does not include the Access-Control-Allow-Origin header matching your page's origin. The fix is always on the server side — add the appropriate CORS headers. You cannot fix a CORS error from the client-side JavaScript alone (unless you proxy through your own server).
What is a CORS preflight request?
A preflight is an automatic OPTIONS request the browser sends before a "non-simple" cross-origin request (e.g., PUT, DELETE, or any request with custom headers like Authorization or Content-Type: application/json). The browser checks whether the server permits the actual request before sending it. Your server must respond to OPTIONS with the correct Access-Control-Allow-* headers and a 2xx status.
Can I use * with Access-Control-Allow-Credentials?
No. When the client uses credentials: 'include' in fetch (to send cookies or HTTP auth), the server must respond with the exact requesting origin in Access-Control-Allow-Origin — the wildcard * is explicitly disallowed. Validate the incoming Origin request header against your allowlist and echo it back.