skip to content

CORS from the Frontend

You will learn to read and fix a CORS failure from the browser side: which request triggered a preflight, why credentials break wildcards, and why you cannot patch it in client code. Interviewers ask this because nearly every frontend engineer has debugged it badly at least once.

on this pageshow

questions

5

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?

level: juniorimportance: must knowfreq 82%

answer

  1. the browser is the one complaining
  2. request never sent the permission
  3. response headers, not request headers
  4. curl has no origin to protect
  5. opt-in lives on the server side

basics

~20 s

The 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 s

That 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

A cross-origin fetch to your API worked until you added credentials: 'include' so the session cookie would be sent; now the browser rejects the response and complains about the wildcard in Access-Control-Allow-Origin. What must the API return instead, and why does '*' stop being acceptable once credentials are involved?

level: middleimportance: must knowfreq 68%

basics

~20 s

A credentialed cross-origin response must name the exact calling origin in Access-Control-Allow-Origin and also send Access-Control-Allow-Credentials: true. The wildcard is banned there because '*' means any site may read the response — which, with the user's cookies attached, would expose their logged-in data to every origin.

open as a page

A cross-origin POST from your page fails with a CORS error in the console, yet the record was created on the server anyway. Explain why some cross-origin fetches reach the server before the browser has any permission, and what in your own fetch call decides that the browser asks first instead.

level: middleimportance: must knowfreq 62%

basics

~20 s

CORS controls whether a page may read a cross-origin response, not whether the request is sent. Requests that a plain HTML form could already make are sent immediately and can have side effects; anything beyond that — a method like PUT or DELETE, a Content-Type of application/json, or a custom header — makes the browser check with the server first.

open as a page

To silence a CORS error, a teammate adds mode: 'no-cors' to a fetch() call and reports that it now resolves without complaint. What does the browser actually hand back in that mode, and when is no-cors a legitimate choice rather than a way of hiding a bug?

level: seniorimportance: should knowfreq 42%

basics

~20 s

no-cors returns an opaque Response: status reads as 0, headers are empty, ok is false, and the body cannot be read. It never grants access to data — it only stops the error. It is legitimate only for fire-and-forget requests or caching a resource you never need to inspect.

open as a page

Your single-page app is served from one origin and its API from another, and calls carry the user's session. How would you decide between keeping that cross-origin arrangement with CORS and serving the API from the app's own origin through a reverse proxy path such as /api?

level: principalimportance: should knowfreq 34%

basics

~20 s

Same-origin proxying removes CORS, advance permission checks and cross-site cookie exposure entirely, at the cost of an extra hop you must operate. Cross-origin with CORS is right when many independent or third-party clients need the API; a first-party SPA with a session usually belongs on one origin.

open as a page