CORS Explained for Frontend
Why the browser blocks cross-origin responses, what Access-Control-* headers mean, preflight, credentials, and how to fix real frontend errors.
- browser
- cors
- security
- fetch
- http
CORS is not a server firewall and not “encryption.” It is a browser-enforced rule set: JavaScript in origin A may not read the response from origin B unless B opts in with the right HTTP headers.
Your curl works. Postman works. The frontend fails with a red Network panel. That gap is CORS (or a misdiagnosed network error that looks like CORS).
Specs/docs: MDN CORS, Fetch Standard, Same-origin policy.
Origin in one line
An origin is scheme + host + port:
| URL | Origin |
|---|---|
https://app.example.com |
https://app.example.com |
https://app.example.com:443 |
same as above (default port) |
http://app.example.com |
different (scheme) |
https://api.example.com |
different (host) |
https://app.example.com:3000 |
different (port) |
Same site ≠ same origin. www vs apex are different origins.
What CORS actually allows
Without CORS headers, the browser can often send a simple request (you may see it in Network), but your JS cannot read the body or headers. The console error is about access, not necessarily about the request never leaving the machine.
Cross-origin embeds (<img>, <script>) follow different rules. CORS is about reading cross-origin responses from script (XHR/fetch, fonts in some cases, WebGL textures, etc.).
Simple requests vs preflight
Simple enough to skip preflight (historical “simple request”)
Roughly: method is GET, HEAD, or POST; headers are limited to a safe set (Accept, Content-Language, Content-Type with certain values like text/plain, multipart/form-data, application/x-www-form-urlencoded); no fancy custom headers.
Preflight (OPTIONS)
If you use PUT/PATCH/DELETE, custom headers (Authorization, X-Request-Id), or Content-Type: application/json, the browser first sends:
OPTIONS /v1/items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
Server must answer with something like:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 7200
Only then does the browser send the real request.
Frontend fix for “preflight fails” is almost always backend or gateway config, not a React change. Dev proxies only hide the problem in local setup.
Headers you’ll actually touch
| Header | Who sends | Meaning |
|---|---|---|
Origin |
Browser | Requesting page’s origin |
Access-Control-Allow-Origin |
Server | Which origin may read the response (* or a specific origin) |
Access-Control-Allow-Credentials |
Server | Allow cookies/auth in cross-origin requests |
Access-Control-Allow-Methods |
Server | Methods allowed after preflight |
Access-Control-Allow-Headers |
Server | Request headers allowed after preflight |
Access-Control-Expose-Headers |
Server | Response headers JS may read (beyond safe defaults) |
Access-Control-Max-Age |
Server | Cache preflight (seconds) |
Credentials and *
fetch('https://api.example.com/me', {
credentials: 'include', // send cookies
});
If credentials are included, the server must not respond with:
Access-Control-Allow-Origin: *
It must echo a specific origin and set:
Access-Control-Allow-Credentials: true
Also requires correct SameSite/cookie attributes — CORS alone doesn’t invent cookies.
Minimal mental model for fixing bugs
- Open DevTools → Network.
- Is there a failed
OPTIONS? Fix allow-methods/headers/origin on the API. - Is the real request status 200 but JS throws? Missing
Access-Control-Allow-Originon the response. - Is the real request 401/500? That’s an API error; CORS message can appear if the error response also lacks ACAO headers — fix both status and CORS on error paths.
- Local SPA on
localhost:5173talking tolocalhost:8080is cross-origin. Use a dev proxy or real CORS config.
Example Express-ish response for a trusted SPA:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Access-Control-Allow-Credentials: true
Use Vary: Origin when the ACAO value depends on the request origin so caches don’t poison another site.
What frontend engineers should not do
- Disable the same-origin policy with a browser plugin for “production fixes.”
- Wildcard + credentials — browsers reject it.
- Assume CORS protects the API from non-browser clients — it doesn’t. Use real authz.
- Mirror
Originblindly to any value — that’s “allow everyone” with extra steps; allowlist.
Related platform pieces
- Same-origin policy — the baseline
- Preflight OPTIONS — deeper preflight
- Cookies + CSRF — separate from CORS; don’t confuse them
- CDN/gateway — often where ACAO is stripped on error pages
Interview angle
Q: Why does Postman work but the browser doesn’t?
A: CORS is enforced by browsers on credentialed script access; other clients don’t apply it.
Q: What is a preflight?
A: An OPTIONS probe for non-simple requests so the server can opt into methods/headers before the real call.
Q: Can Access-Control-Allow-Origin: * work with cookies?
A: No. Specific origin + Allow-Credentials: true.
Draw the sequence: JS → (OPTIONS?) → request → browser checks ACAO → expose body to JS or block.
Related on this site
- Same-origin policy
- Preflight OPTIONS requests
- Browser networking 101
- HTTP caching headers
- XSS for Frontend Engineers
- useEffect Fundamentals — fetch + abort patterns
Further reading
Related guides
- Content-Type and MIME SniffingWhy Content-Type matters, how MIME sniffing works, X-Content-Type-Options: nosniff, and XSS-ish pitfalls.
- Preflight OPTIONS RequestsWhen browsers send CORS OPTIONS preflights, which headers to return, caching Max-Age, and SPA debug tips.
- Same-Origin PolicyOrigin tuple (scheme/host/port), what SOP blocks, and how CORS, postMessage, and cookies relate.
- Browser Networking 101DNS, TCP/TLS, HTTP/1.1 vs H2/H3, connection reuse, and what frontend code can actually influence.
- Cross-Origin Embedder PolicyCOEP require-corp / credentialless: cross-origin isolation, SharedArrayBuffer, and embedding requirements.