The course
Workarounds & proxies
Every page so far has treated CORS as a fact about the browser you have to design your server around. This page is about the four things developers reach for when they want to not design around it. Three are traps, in three different ways; one is a real technique with a narrow, honest use that this site itself demonstrates.
1. The dev-server proxy: a trap that breaks on deploy
Vite, webpack-dev-server, and Create React App all let you add a small
proxy entry to your dev config — a line like
server.proxy: { "/api": "http://localhost:9000" }
in Vite, or the equivalent proxy key in webpack and CRA.
During local development, a request from your page (served on, say,
localhost:5173) to /api/users is transparently
forwarded by the dev server to a backend on
localhost:9000. From the browser's point of view that
request is same-origin — it goes to localhost:5173/api/users
— and the dev server does the cross-port hop on its own. No
cross-origin request is ever made, so there is nothing for CORS to
govern: no preflight, no Access-Control-Allow-Origin, no
withheld response.
The trap is that this arrangement exists only in your local dev
server. When you build and deploy, the frontend is served by one real
host (www.yoursite.com) and the backend by another
(api.yoursite.com) — two distinct origins — and there is no
dev server in between to quietly absorb the difference. The app that
worked perfectly on your laptop now fails in a real browser with a vague
TypeError: Failed to fetch, because the cross-origin
response the real deployment now makes is missing the CORS headers the
proxy had silently made irrelevant.
A dev-server proxy does not solve CORS. It hides it — and only locally. The same deployment that exposes the real cross-origin hop also exposes the fact that the backend was never configured to answer it. This is also why the pages on this site that let you watch a real CORS failure happen (pages 2, 3, 6) are the load-bearing ones: a failure you can reproduce is a failure you actually understand.
2. The same-origin reverse proxy (this site's own architecture)
The legitimate technique is to stop making the request cross-origin at all — serve your API from the same origin as your front end, by putting a reverse proxy (nginx, a CDN behavior, an API gateway) in front of it. This site does exactly that, for one specific page, and you can read it straight off the shipped infrastructure.
CloudFront, in front of this site, has a behavior for
/same-origin-api/* that proxies those requests verbatim —
no path rewriting — to the demo API's own custom domain. The Hono API
app is mounted twice (once at /, once under that same
/same-origin-api prefix), so the same routes answer at
either path. Locally, one scoped vite proxy is the faithful analog of
that CloudFront behavior, forwarding the same prefix to the dev API
server — the single sanctioned exception to the rule in the box above.
The result is that two URLs reach the identical backend Lambda:
-
https://www.corslabs.com/same-origin-api/demo/echo— same origin as this page. The browser sees a request towww.corslabs.com(the origin the page itself came from) and does not apply CORS at all. NoOPTIONSpreflight, noAccess-Control-Allow-Origincheck, no withheld response. -
https://api.www.corslabs.com/demo/echo— a different origin. The browser applies the full CORS protocol; the response is only readable if the server opts in.
Both requests hit the same Lambda and return byte-identical responses. The only difference is the browser's own rule about which one it will let the page read without a grant. The diagram below shows how the two paths converge on one backend:
This genuinely removes CORS from the path it applies to. But notice what
it does not do: it does not let a cross-origin request skip
CORS. It just makes one specific path same-origin. The cross-origin API
on api.www.corslabs.com is still needed for every other
page on this site, and it still enforces CORS exactly as the last eleven
pages demonstrated. In fact this site uses the same-origin path on
exactly one page — page 1's origin comparator — as a
control group: the request the browser does not gate,
set beside the one it does, so the difference is observable rather than
merely asserted.
3. Third-party CORS proxies: a real risk, not a shortcut
The third pattern is to skip configuring your own backend entirely and
point your fetch at a public "CORS proxy" — a third-party
service that fetches the real resource on its server and
returns it to the browser with
Access-Control-Allow-Origin: * attached. The browser sees a
CORS-permissive response and lets your page read it. It appears to fix
the problem with no server-side work on your end.
The reason it is a real risk is that the proxy operator is now a full
participant in every request and response that flows through it. The
browser sent the request to the proxy, not to the real origin; the proxy
then makes its own request to the real origin and hands the response
back. The operator can read every header and every body in both
directions — and can log, cache, modify, or withhold any of it. If the
request carries credentials, an Authorization header, a
session cookie, or the response contains private data, all of it passes
through a server you do not control, run by a party you have no contract
with and no recourse against.
This is not an alarmist framing. A CORS proxy is, by construction, a machine-in-the-middle that you have deliberately introduced between your page and the resource it reads. There are legitimate uses — a public, read-only proxy fetching open data with no credentials, for a quick throwaway prototype — but the moment real traffic, credentials, or any non-public data is involved, pointing it at a third-party proxy is extending your trust boundary to cover an unknown operator. The server-side fixes on page 7 are a few response headers; this is never worth trading those few headers for.
4. Browser flags and extensions are never the answer
The last "workaround" is to fix nothing on the server, and instead turn
CORS off in the browser you test with. Chrome's
--disable-web-security flag, or any of several "CORS
Unblock" browser extensions, makes the developer's own browser stop
enforcing the policy — and the failing request now succeeds.
The reason this is not a fix is that it changes only your browser. The CORS policy that matters is the one a real visitor's unmodified browser enforces against your deployed site, and that browser has not been told to disable anything. A bug masked this way ships broken and stays broken for every real user — discovered, if at all, by someone else.
The second cost is the one developers underweight:
--disable-web-security is not scoped to your dev site. It
turns off a core browser security control for the entire browsing
session — every other origin you visit in that browser, for as long as
it runs, loses the same-origin protection that prevents unrelated sites
from reading each other's responses. Normalizing a daily-driver browser
in that state, to avoid configuring a handful of response headers, is a
poor trade. Use it for a one-off, isolated test profile if you must;
never treat it as the way your development environment works, and never
treat a problem that disappears under it as actually solved.