CORS is the browser's rule, not the server's
The one fact that turns CORS from baffling to obvious: it is enforced by the
browser, not the server. When your JavaScript on app.example.com fetches from
api.other.com and gets a CORS error, it is tempting to think the request was
blocked. Usually it was not — for a simple request the browser sent it, the
server received and ran it, and a response came back. The browser then checked
whether that response carries a header permitting your origin to read it, found it
missing, and refused to hand the response to your code. The data existed; the
browser withheld it from your script. That is CORS: a client-side gate on reading
cross-origin responses, not a lock on the server’s door.
The server grants permission with a header
The only thing the server controls is whether it permits your origin to read the
response, which it declares in the Access-Control-Allow-Origin header. Without
it, the browser blocks the read; with it matching your origin, the browser lets the
data through:
GET /data HTTP/1.1
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com ← the browser now allows the read
Content-Type: application/json
Note where the header lives: on the response, from the API server. Your frontend cannot add it — which is exactly why “just fix CORS in my React code” is not a thing.
The preflight: an OPTIONS you did not write
For anything beyond a “simple” request — a custom header, a PUT/DELETE, a JSON
content type — the browser sends a preflight OPTIONS request first, asking
permission before the real one. The server must answer the preflight with the
methods and headers it allows, or the real request never leaves:
OPTIONS /data HTTP/1.1
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: content-type
This is why a request “fails before it is sent” — you are seeing a preflight the server did not satisfy, and the browser correctly declining to make the real call.
Why the rule exists, and what it does not protect
CORS exists because browsers automatically attach the user’s cookies to requests,
so without a rule, any site you visit could quietly call your bank’s API as you
and read the response. The same-origin policy blocks that read by default; CORS is
the mechanism a server uses to relax it for origins it trusts. Two things follow.
First, CORS protects the user’s browser data from other sites, not the API from
attackers — a script, a mobile app, or curl faces no CORS at all, so it is never
a substitute for real auth. Second, because only the API server can grant access,
your two real fixes when you do not control that server are to proxy the call
through your own origin or to get the API to send the headers. Knowing CORS is a
browser read-gate, not a server lock, is what makes those fixes obvious instead of
mysterious. The proxy-and-cors exercise walks through exactly that decision.