CORS Errors Explained for Frontend Developers
That red console line isn’t your fetch “being weird.” It’s the browser enforcing origin rules. Here’s how to read the error and fix the right layer.
Part of the Technical Foundations series.
The UI looks fine until the network panel turns red: “blocked by CORS policy.” Frontend Twitter lore says “just slap * on the API.” That’s how you “fix” local dx and invent a production footgun.
CORS is the browser protecting users, not the server blocking you out of spite. Understanding which hop failed saves hours.
What CORS is not
It is not a primary auth mechanism. It does not stop non-browser clients from calling your API. Curl never cared about your Access-Control-Allow-Origin. CORS only constrains web pages reading responses across origins.
Simple requests vs preflight
“Simple” requests (certain methods and headers) go straight to the server. Others trigger an OPTIONS preflight. If preflight fails, your real POST never happens - which is why logs look empty while the console screams.
Custom headers like Authorization or Content-Type: application/json usually mean preflight. Design for it.
Reading the error without panic
- No ACAO header - server didn’t opt in for your origin.
- ACAO is
*with credentials - illegal combo; browser rejects it. - Method not listed -
Access-Control-Allow-Methodsmissing your verb. - Header not listed - expose/allow headers mismatch.
Fix the right layer
- Local dx: Vite/Webpack proxy to same origin so the browser never crosses origins in dev.
- API config: allowlist exact origins for deployed frontends. Draft headers with CORS Generator.
- CDN/gateway: make sure the edge forwards
OPTIONSand doesn’t strip ACAO. - Credentials: if you send cookies, you need
Access-Control-Allow-Credentials: trueand a specific origin - never*.
While you’re in header land, skim Security Headers and Headers Checklist. CORS often ships beside CSP and frame protections - see CSP starter.
Anti-patterns
- Reflecting arbitrary
Originvalues back without an allowlist. - Disabling CORS via browser extensions and calling it fixed.
- Using
*in production “temporarily.” It becomes permanent.
A debug transcript you can reuse
- Reproduce with DevTools open; note the failing request and whether OPTIONS appears.
- Inspect response headers on OPTIONS and on the real method.
- Compare
Originto ACAO exactly - schemes and ports matter (http://localhost:5173≠http://127.0.0.1:5173). - If credentials are involved, confirm ACAO is explicit and ACAC is true.
- Fix server allowlists or introduce a same-origin proxy; retest on a clean browser profile without CORS-unpinning extensions.
The BFF pattern as an escape hatch
A backend-for-frontend on the same origin as the SPA avoids browser CORS for that path. The BFF talks to upstream APIs server-side. Complexity moves to your server, which is often where auth belonged anyway. Use it when you control the stack; don’t invent a BFF solely to paper over a misconfigured API you could fix.
Gateway gotchas
API gateways sometimes answer OPTIONS themselves with incomplete headers, or strip ACAO from origin responses. When local Node CORS middleware looks right but production fails, compare gateway configs next - not your React code.
Field notes from teams who shipped this
The pattern that keeps showing up: write the constraint first, then the steps, then the failure modes. Teams that only publish happy-path screenshots create tickets. Teams that document the ugly path create trust.
Schedule a short review ninety days after publishing. Check whether product UI names still match, whether linked tools still exist, and whether support still hears the same questions. Update the page or merge it. Standing still is how useful posts become interchangeable again.
If you adapt this article for internal wikis, keep the examples tied to your stack names. The moment you generalize back to “best practices for organizations,” you’ve started erasing the specificity that made the piece worth saving.
Cookies, SPA auth, and CORS
Cookie-based auth across subdomains needs careful SameSite attributes and explicit origins. Token-in-header designs avoid some cookie CORS pain but introduce storage and XSS concerns of their own - pick consciously.
If you move from local API ports to a unified reverse proxy, many CORS tickets disappear. That’s a hint the problem was architecture, not React.
Write a short internal doc titled “Our origins” listing production, staging, and local URLs that APIs allow. Update it when preview deploy URLs appear - ephemeral Vercel/Netlify origins are a common source of “works on mine” CORS chaos.
Automate allowlist updates where you can, but never auto-allow * for credentialed routes.
Partner APIs you don’t control
When a third party won’t add your origin, you need a server-side proxy or a partner change request. Front-end-only “fixes” won’t exist. Start that conversation early in the integration timeline, not the day before launch.
Track partner CORS contacts beside the API keys in your vault notes.
PR checklist for API CORS changes
- Listed origins match deploy previews you still care about.
- Credentialed routes never use wildcard ACAO.
- OPTIONS returns quickly without auth challenges that block preflight.
- CDN config reviewed if you use one.
- Smoke test from the real frontend origin, not only Postman.
Postman success never proved CORS. Browsers do. Keep a simple HTML smoke page in the repo if you need a minimal reproducer without booting the full SPA.
FAQ
Why does the mobile app work but the SPA doesn’t?
Native apps aren’t browsers enforcing CORS. The API was fine; the web origin wasn’t allowed.
Why do local HTML files fail?
file:// origins are awkward. Serve over http://localhost instead.