CORS: why the browser blocked you
Your server returned 200 OK, Postman works fine, and your frontend console is red. CORS is not a server error; it is a browser security sandbox.
Your API returned 200 OK. Postman received the payload instantly. Curl prints the JSON cleanly. Yet in your browser, the network tab is red and the console shows the classic message:
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The most common misconception about CORS is thinking your server crashed or refused the connection. It didn’t. The browser blocked your JavaScript from reading the response.
What Same-Origin Policy actually protects
Browsers enforce the Same-Origin Policy (SOP). An origin consists of three parts:
https:// api.example.com :443
─────── ─────────────── ────
Scheme Host Port
If any of those three differ between where your frontend is hosted (https://app.example.com) and where your API lives (https://api.example.com), the request is cross-origin.
Without SOP, any malicious website you visit in another tab could run fetch('https://your-bank.com/api/balance'). The browser would automatically attach your session cookies, and the rogue script could read your bank balance and exfiltrate your private data.
CORS is the exception mechanism
CORS (Cross-Origin Resource Sharing) is how a server gives the browser explicit permission to relax SOP for trusted origins.
When your frontend makes a cross-origin request, the browser automatically attaches an Origin header:
GET /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
If the server wants to allow the frontend to read this response, it must respond with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
If that header is missing (or does not match the requesting origin), the browser discards the response and raises a CORS error in the JavaScript console.
Preflight requests (OPTIONS)
For requests that could mutate data or use non-standard headers (like PUT, DELETE, PATCH, or custom headers like Authorization or Content-Type: application/json), the browser sends a preflight check before sending the actual request:
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
The server must approve the preflight:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Access-Control-Max-Age caches the preflight response so the browser doesn’t have to send an OPTIONS request on every single API call.
The common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
Access-Control-Allow-Origin: * with credentials | Browsers reject wildcard origins when credentials: 'include' is set | Reflect the specific allowed origin |
| Handling CORS in frontend code | Frontend JavaScript cannot bypass browser SOP | Configure headers on the API gateway or backend |
Missing OPTIONS route handler | Preflight fails with 404/405 before actual request ever fires | Return 204 No Content on OPTIONS routes |
| Dropping CORS on error responses (500/404) | Hides real server error behind a confusing CORS warning | Apply CORS middleware globally before error handlers |
Test Your Understanding
Quick architectural scenario challenge based on this breakdown.
Your React frontend at app.company.com makes a POST request with JSON payload to api.company.com.
Q1.Why does the browser send an HTTP OPTIONS preflight request before a POST request with 'Content-Type: application/json'?
Comments –
Sign in to share your thoughts
Nothing yet. Be the first.