Frontend Labs
⋯

28. CORS in depth: same-origin policy, preflight, credentials

The same-origin policy is the browser's foundational security boundary: scripts from https://a.example.com cannot read responses from https://b.example.com unless b.example.com explicitly opts in via CORS headers. "Origin" is the exact triple (scheme, host, port) — http:// vs https://, or a different port, is a different origin even on the same domain.

// From https://app.example.com, this request goes out on the wire regardless —
// CORS does NOT stop the request from being SENT. It stops the BROWSER from
// letting your JS READ the response, unless the server says it's allowed to.
fetch("https://api.other.com/data");

The server must respond with Access-Control-Allow-Origin: https://app.example.com (or * for fully public APIs) or the browser blocks JS from reading the response — the response bytes did arrive over the network; CORS is enforced by the browser, not the server or the network, which is why CORS errors are invisible to tools like curl.

A preflight request (OPTIONS) is sent automatically by the browser before certain requests — non-"simple" methods (PUT, DELETE, PATCH), or custom headers like Authorization — to ask the server's permission before sending the real request with credentials or a non-trivial body:

Browser preflight (automatic, you never write this):
  OPTIONS /api/users/42
  Access-Control-Request-Method: DELETE
  Access-Control-Request-Headers: authorization

Server must respond:
  Access-Control-Allow-Origin: https://app.example.com
  Access-Control-Allow-Methods: DELETE
  Access-Control-Allow-Headers: authorization

Only THEN does the browser send the actual DELETE request.

Sending cookies cross-origin requires both sides to opt in explicitly — a common source of "my auth cookie isn't being sent" bugs:

fetch("https://api.example.com/me", { credentials: "include" });
// AND the server must respond with:
//   Access-Control-Allow-Credentials: true
//   Access-Control-Allow-Origin: https://app.example.com   (NOT "*" — wildcard is
//                                                             forbidden together with credentials)

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36