Fixing CORS Errors: Access-Control-Allow-Origin Explained
Why the browser throws CORS errors and how to fix each case — preflight, credentials and the wildcard trap.
Your frontend calls an API and the console lights up with blocked by CORS policy — yet the exact same request works perfectly in Postman or curl. The natural instinct is to blame the server or a firewall, but that is looking in the wrong place. CORS errors are enforced by the browser, not the server. In most cases the request actually reaches the server and gets processed; the browser receives the response, finds no header granting permission, and throws it away before your JavaScript ever sees it.
That means the fix almost never lives in your frontend code — it lives in the response headers your server sends back. This guide walks through what the same-origin policy actually is, how to read each error message, when the browser fires an OPTIONS preflight, and exactly which headers to add in each scenario — including the credentials-plus-wildcard trap that catches almost everyone at least once.
The same-origin policy: it is the browser blocking you
Browsers enforce the same-origin policy as a core security boundary. An origin is the combination of scheme://host:port — if any one of the three differs, it is a different origin. A page on https://app.example.com calling https://api.example.com is cross-origin (different host). So is http://localhost:3000 talking to http://localhost:8080 (different port).
Without this policy, any malicious page could use your logged-in browser session to silently read your bank or email APIs. So the browser blocks scripts (fetch/XHR) from reading cross-origin responses by default, and only relaxes that when the server explicitly opts in via CORS (Cross-Origin Resource Sharing) response headers. Two facts worth internalizing:
- Postman, curl, and server-to-server calls never hit CORS errors, because the same-origin policy exists only inside browsers. “It works in curl but not in the browser” is not a bug — it is the literal definition of a CORS failure.
- CORS is not server protection. For simple requests the server has already executed the request; the browser merely withholds the response from JavaScript. If you need access control, use authentication and authorization — not CORS.
Read the error message — it tells you which fix you need
Chrome’s console message already names the missing piece. Different wordings point to different root causes, so match the exact phrase before changing anything.
No 'Access-Control-Allow-Origin' header is present— the response carries no CORS header at all. Either CORS is not configured, or your error responses (4xx/5xx) skip the CORS middleware....header has a value '...' that is not equal to the supplied origin— CORS is configured, but your origin is not on the list. Watch forwwwvs bare domain, http vs https, and mismatched ports.Response to preflight request doesn't pass access control check— the failure happens at the OPTIONS preflight stage, not the real request. Typical causes: OPTIONS returns 404/401, orAllow-Methods/Allow-Headersare incomplete....must not be the wildcard '*' when the request's credentials mode is 'include'— you are sending cookies while the server answers with*. That is the wildcard trap covered below.
Simple vs preflighted requests — when OPTIONS fires
The browser splits cross-origin requests into two classes. A simple request is sent immediately and only the response headers are checked. Anything outside the simple criteria triggers an automatic OPTIONS preflight first, asking the server for permission before the real request goes out. The simple criteria are narrower than most people expect:
- Method is
GET,HEAD, orPOST - Only safelisted headers are set manually:
Accept,Accept-Language,Content-Language,Content-Type Content-Typeis one ofapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain
Which means the requests real apps make all the time — application/json POSTs, anything with an Authorization header, any PUT/DELETE/PATCH — are all preflighted. Here is the full exchange for a SPA on https://app.example.com calling an API with a bearer token.
Step 1 — the browser sends the preflight on its own:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-typeStep 2 — the response the server must return (status 200 or 204, no body required):
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400Step 3 — only then does the browser send the actual POST, and that response must also carry Access-Control-Allow-Origin: https://app.example.com for JavaScript to read it. Two classic traps here: the headers are required on both the preflight and the real response, and your auth middleware must not reject the OPTIONS request with 401 — preflights never carry the Authorization header. Setting Access-Control-Max-Age lets the browser cache the preflight result so OPTIONS does not fire on every single call.
Scenario-by-scenario header cheat sheet + the wildcard trap
| Scenario | Response headers the server must send |
|---|---|
| Public GET API (no cookies, no custom headers) | Access-Control-Allow-Origin: * is enough |
| JSON POST / PUT / DELETE / custom headers (preflighted) | Real response: Allow-Origin. OPTIONS response: Allow-Origin + Access-Control-Allow-Methods + Access-Control-Allow-Headers |
Cookies / sessions (credentials: "include") | Access-Control-Allow-Origin: <echo the request Origin> (never *) + Access-Control-Allow-Credentials: true + Vary: Origin |
| JS needs to read custom response headers | Access-Control-Expose-Headers: X-Request-Id, ... |
The wildcard trap: the moment a request includes credentials (credentials: "include" in fetch, or withCredentials in XHR), the browser categorically rejects Access-Control-Allow-Origin: *. In credentialed mode, * in Allow-Headers and Allow-Methodsis also treated as a literal header/method named “*”, not a wildcard. The correct fix is for the server to check the incoming Origin header against an allowlist and echo that exact value back, adding Vary: Originso caches never serve one origin’s response to another. Never echo arbitrary origins unconditionally on a credentialed API — that effectively opens your cookie-authenticated endpoints to every website on the internet.
Verify the fix with real measurements
After changing server config, do not guess — inspect the actual headers on the wire. A practical order:
- Run the API URL and your calling origin through the CORS checker to see, from both the preflight and the actual-request perspective, which permission headers come back and which are missing.
- Use the HTTP headers inspector to confirm the
Access-Control-*family andVaryare present on the live response. This is also where you catch CDNs or reverse proxies silently stripping headers your origin server sets. - To reproduce the preflight by hand, build an
-X OPTIONSrequest withOriginandAccess-Control-Request-Methodheaders in the curl command builder and read the raw server response — free of browser caching, including the preflight cache.
One last tip: make sure error responses (401, 500, …) also carry CORS headers. If they do not, every real failure looks like “a CORS error” in the browser, hiding the actual cause and making debugging far harder than it needs to be.
Frequently asked questions
Why does my request work in Postman and curl but fail with CORS in the browser?
Can I fix a CORS error from the frontend by changing fetch options?
When exactly does the browser send an OPTIONS preflight?
I set Access-Control-Allow-Origin: * but requests are still blocked. Why?
Preflights fire on every request and slow things down. Can I reduce them?
Tools to use with this guide
Related guides
- Essential Security Headers Guide (CSP, HSTS, X-Frame-Options)The security headers you must set, what each does, recommended values and common mistakes.
- 301 vs 302 vs 307/308: Choosing the Right RedirectThe difference between redirect status codes and how to pick the right one for SEO and method preservation.