ESC

Type to search the knowledge base.

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.

intermediate4 min read
  • 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

  1. Open DevTools → Network.
  2. Is there a failed OPTIONS? Fix allow-methods/headers/origin on the API.
  3. Is the real request status 200 but JS throws? Missing Access-Control-Allow-Origin on the response.
  4. 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.
  5. Local SPA on localhost:5173 talking to localhost:8080 is 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 Origin blindly to any value — that’s “allow everyone” with extra steps; allowlist.
  • 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.

Further reading

Related guides