Fixing CORS Errors — Access-Control-Allow-Origin Explained
A CORS error in the browser console means the response lacked permission headers the browser required for your JavaScript to read it — or the preflight failed. It does not mean axios is broken. This article is a hands-on fix guide: read the error, confirm headers, patch the server or edge, and stop applying folklore in the client.
For the related “works on localhost only” story, see the production-proxy angle in other posts — here we focus on making a correct CORS configuration.
What the browser is asking
When a page at https://app.example.com calls https://api.example.com, the browser sends an Origin header. For the SPA to read the response, the API must answer with something like:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
If cookies or Authorization with credentialed mode matter:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
* plus credentials is illegal and will fail. Reflect an allowlisted origin instead.
Simple request vs preflight
“Simple” GETs with limited headers may skip OPTIONS. JSON POST with Content-Type: application/json usually triggers preflight:
OPTIONS /v1/items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,authorization
Your server must respond with allow headers covering method and requested headers, then succeed on the real POST with ACAO again.
Express example
import cors from "cors";
const allowlist = new Set([
"https://app.example.com",
"http://localhost:5173",
]);
app.use(
cors({
origin(origin, cb) {
if (!origin || allowlist.has(origin)) return cb(null, true);
return cb(new Error("Not allowed by CORS"));
},
credentials: true,
}),
);
Ensure error middleware still emits CORS headers; otherwise 500s become opaque CORS failures in DevTools.
nginx snippet
set $cors_origin "";
if ($http_origin ~* ^https://(app\.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,PATCH,DELETE,OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
The always flag matters so error responses keep headers. Test carefully — if in nginx has footguns; many teams prefer the API app to own CORS.
Frontend checklist (what you can and cannot do)
| Action | Useful? |
|---|---|
Set mode: 'cors' | Default for cross-origin reads; fine |
Set credentials: 'include' when cookies needed | Yes, with matching ACAO |
Add Access-Control-Allow-Origin from JS | No — response header only |
| Browser extension “Allow CORS” | Dev theater; not a production fix |
| Proxy via your backend | Yes when you cannot change upstream |
Debugging sequence
- Open Network → failed request → Response headers.
- If OPTIONS failed, fix preflight first.
- Compare
OrigintoAccess-Control-Allow-Origincharacter-for-character. - curl the OPTIONS and GET/POST with
Originset. - Confirm CDNs do not strip ACAO or cache the wrong origin (
Vary: Origin). - Retry from the real app origin, not only Postman.
curl -i -X OPTIONS "https://api.example.com/v1/items" \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type"
Security notes
Allowing * on credentialed cookie APIs is not just invalid — reflecting arbitrary origins on authenticated endpoints is dangerous. Maintain an allowlist. Prefer same-site architectures (BFF) when cookies are central.
Common misconfigurations
| Mistake | Result |
|---|---|
| Allowlist only localhost | Prod app blocked |
| ACAO on success only | Errors look like CORS |
| WAF blocks OPTIONS | Preflight dies |
| Trailing slash / www mismatch | Intermittent failures |
Bottom line
CORS is fixed on the server or edge by returning precise allow headers for your app’s origin, including preflight and error paths. Read the Network panel, curl OPTIONS, allowlist origins, and ignore client-side “CORS header” myths.