// page 12

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.

This site's own standing rule. The engineering notes for corslabs itself carry one non-negotiable about local development: there is no dev proxy in any mode for any actual CORS demo on this site. Every cross-origin demo you have run on the previous pages made a real, unproxied cross-origin request in dev, preview, CI, and prod — because the whole point of those demos is to show CORS actually doing something. There is exactly one narrow, sanctioned exception to that rule, and it is the subject of section 2.

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 to www.corslabs.com (the origin the page itself came from) and does not apply CORS at all. No OPTIONS preflight, no Access-Control-Allow-Origin check, 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:

www.corslabs.com the page's origin /same-origin-api/* via CloudFront — same origin api.www.corslabs.com direct — cross-origin demo API Lambda identical responses same origin — no CORS cross-origin — CORS enforced

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.