A fetch() call from a page on https://app.example.com to https://api.other.com fails with a console message saying the response was blocked by CORS policy. Who produced that error, and why does adding an Access-Control-Allow-Origin header to the request headers of your fetch call not fix it?
answer
- the browser is the one complaining
- request never sent the permission
- response headers, not request headers
- curl has no origin to protect
- opt-in lives on the server side
basics
~20 sThe browser produced the error, not the server and not your code. CORS permission is granted by headers on the server's response, so Access-Control-Allow-Origin can only be set by the API. Sending it as a request header changes nothing.
solid answer
~50 sThat message comes from the browser's own enforcement, after the response arrived. When a page makes a cross-origin request with `fetch`, the browser attaches an `Origin` header and then checks the response for an `Access-Control-Allow-Origin` value that matches that origin. If it is missing or does not match, the browser refuses to hand the response to my JavaScript and rejects the promise with a `TypeError` — I never see the status or the body. `Access-Control-Allow-Origin` is a *response* header, so putting it in my own request headers is meaningless; worse, adding an unrecognised header can turn a request that was previously allowed straight through into one the browser checks in advance. The fix is always on the server or the edge in front of it: it has to say my origin is allowed. That is also why the same call works fine from curl — CORS is a browser rule, not a network rule.
go deeper
Be ready to say plainly that the browser generates the error and the server grants the permission. Know that Access-Control-Allow-Origin is a response header and that the fix is never in your fetch options.
Explain the mechanics: the browser attaches the Origin header, matches it against Access-Control-Allow-Origin on the reply, and rejects with an opaque TypeError so no status or body leaks to the page.
Show you can debug it — read the real status in the Network panel, recognise that a missing header on an error path masks a 500, and pick between configuring the API, proxying it onto your own origin, or moving the call server-side.
Own the policy: who may configure allowed origins, how preview and staging origins are handled without drifting into a permissive wildcard, and why CORS is never a substitute for authentication and authorization on the API itself.
## What the message is really saying A console line of the form "Access to fetch at 'https://api.other.com/x' from origin 'https://app.example.com' has been blocked by CORS policy" is written by the browser. It is not a network failure, not an error the server sent, and not something your code threw. The browser is reporting that it made a cross-origin request, examined the reply, and decided your script is not allowed to read it. An origin is the scheme, host and port taken together. `https://app.example.com` and `https://api.other.com` are different origins, so every request between them is cross-origin and subject to this check. So is `https://app.example.com` versus `http://app.example.com`, and versus `https://app.example.com:8443`. ## Who grants permission Cross-origin reads are denied by default and the *server* opts in. When the browser sends a cross-origin request from a page, it adds an `Origin` request header naming the calling page's origin — your code cannot set or forge that header, the browser owns it. The server answers with, among the ordinary response headers, `Access-Control-Allow-Origin`. If that value is the exact origin string the browser sent (or `*`, for non-credentialed requests), the browser hands the response to your code. If the header is absent, misspelled, or names a different origin, the browser discards the response. The key asymmetry: `Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`, `Access-Control-Allow-Headers` and `Access-Control-Allow-Methods` are all **response** headers. There is no request header a page can send that grants itself permission — if there were, the whole mechanism would be worthless, because an attacker's page would simply send it too. ## What your code observes When the check fails, `fetch` rejects with a `TypeError` whose message is deliberately vague. You get no status code, no headers, and no body: ```js try { const res = await fetch("https://api.other.com/data"); console.log(res.status); // never runs on a CORS failure } catch (err) { console.log(err instanceof TypeError); // true — and that is all you learn } ``` The vagueness is intentional. Telling the page "it returned 401" or "it returned 500" would already be a cross-origin information leak, so the browser reports one undifferentiated failure and puts the detail only in the console, where a human — not the script — can read it. This is also why a CORS message can *mask* an ordinary bug. If the API throws a 500 and the error path of its framework does not attach the CORS headers that the success path attaches, the browser reports a CORS failure. The real problem is the 500. The Network panel still shows the response and its status, because DevTools is not bound by the page's origin — read the status there or in the server's own logs rather than trusting the console text. ## Why the same call works outside the browser curl, Postman, a mobile app and your backend all speak the same HTTP. None of them is a web page with an origin to protect, so none of them enforces CORS. "It works in Postman" therefore proves only that the endpoint exists; it says nothing about whether a browser will let a page read it. The same-origin restriction exists because a browser carries the user's ambient authority — cookies, network position inside a corporate LAN — into every request a page makes. ## What actually fixes it Three real options, in descending order of how often they are right: 1. **Configure the API** to return `Access-Control-Allow-Origin` for your origin. This is the correct fix when you or your organisation control the API. 2. **Put the call on your own origin** — proxy `https://app.example.com/api/*` through to the upstream service. The browser then sees a same-origin request and never applies the check. This is the standard answer for a third-party API you cannot configure, and it is what a dev server's proxy option does locally. 3. **Call it from your server** instead of the page, when a secret is involved anyway. What does *not* fix it: adding headers to your request, wrapping the call in try/catch, switching from `fetch` to `XMLHttpRequest` or a library like axios (identical rules — the enforcement is in the browser, not the API you call it with), or disabling the check with a browser flag or extension, which fixes only your machine and hides the problem from you until production. One last knob to know but rarely to use: `fetch(url, { mode: "no-cors" })` stops the error appearing, but it does so by giving you an unreadable response rather than by granting access. Silencing the message is not the same as getting the data.
- The endpoint works perfectly in curl and Postman. What does that tell you about the CORS problem?Almost nothing. curl and Postman are not web pages, so they have no origin and enforce no same-origin rules; they will happily show a response that carries no CORS headers at all. The successful call proves the route exists and the payload is right. Whether a browser page may *read* it depends entirely on response headers you should inspect explicitly — for example with `curl -i -H 'Origin: https://app.example.com'` — not on the request succeeding.
- Your API starts returning 500s and the console now shows a CORS error instead. Why, and how do you see the real status?Many frameworks attach CORS headers in a middleware or filter that the error path bypasses, so the 500 response arrives without `Access-Control-Allow-Origin` and the browser reports the CORS failure rather than the status. The CORS message is a symptom. Read the actual status in the DevTools Network panel or the server logs — both see the response the script is denied — then fix the 500 and make the error path emit the same headers.
- Does a CORS failure mean the user's data was protected from the calling page?It means the *response body* was withheld from that page's script. It does not mean nothing happened: the browser still sent the request in many cases, and it says nothing about other channels. CORS protects reads across origins; it is not an authorization mechanism. The API must still authenticate and authorize every request on its own, because non-browser clients ignore CORS entirely.
saying these in an interview costs you the question
- Claims the server sent the CORS error message
- Tries to set Access-Control-Allow-Origin as a request header
- Says CORS blocks the request from ever leaving the browser
- Concludes the API is fine because Postman works
- Proposes a browser flag or extension as the fix