Skip to the content.

CORS is the browser's rule, not the server's

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 request reaches the server and runs; the browser blocks the response from the script JS on app.example.com sends a request to api.other.com, which runs and responds. The browser inspects the response's Access-Control-Allow-Origin header and, if it does not match, blocks the script from reading it. JS onapp.example.com request sent + runs api.other.comresponds 200 response comes back browser checks Allow-Origin → blocks read
The request runs and the server responds; the browser then inspects the response's headers and, absent permission for your origin, refuses to give it to your script.

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.