Home/Learn/Full-Stack Integration/CORS — What Is Actually Happening

CORS — What Is Actually Happening

Intermediate
CORS

The browser blocks your response because you asked another origin for it. CORS is the server's way of saying that is allowed — which is why no amount of frontend code can fix it.

Overview

A CORS error is the first thing that breaks when a frontend and a backend become separate deployables, and it is misunderstood almost universally. The request was not blocked — it usually reached your server, ran, and returned. The browser then refused to hand the response to your JavaScript, because the server did not say this origin may read it. That is the whole mechanism, and it explains the two facts people find hardest to accept: the fix is always on the server, and curl working proves nothing.

The Same-Origin Policy

What an origin is, and what the browser is protecting.

Scheme + host + port, and read vs send
// An origin is scheme + host + PORT. All three must match.
https://aicancode.org        vs  https://api.aicancode.org   // different
http://localhost:3000        vs  http://localhost:8000       // different
https://aicancode.org        vs  http://aicancode.org        // different

// Why it exists: you are logged into your bank. You visit a hostile
// page. Without the same-origin policy, its JavaScript could call
// the bank's API with your cookies attached and READ the response.
// The policy stops the reading.

// Note what it does NOT stop: the request being SENT. A cross-origin
// form POST still reaches the server with cookies attached — that is
// CSRF, and it is a separate problem with a separate fix.

// Not subject to CORS at all — these are cross-origin by design:
<img src="https://other.com/a.png">
<script src="https://cdn.com/lib.js">
<form action="https://other.com/submit" method="post">

// Subject to CORS: fetch, XMLHttpRequest, and anything else that
// lets JavaScript READ the response.

Simple Requests and Preflights

The extra OPTIONS request that appears in the network tab and confuses everybody.

Why POSTing JSON triggers an OPTIONS first
// A "simple" request goes straight out. It qualifies only if:
//   method is GET, HEAD or POST
//   AND the only headers set are a short safe list
//   AND Content-Type is one of:
//       text/plain, application/x-www-form-urlencoded, multipart/form-data

// So this is simple:
fetch('/api/problems')

// And this is NOT — application/json is not on the list:
fetch('/api/problems', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },   // triggers preflight
  body: JSON.stringify(data),
})
// Neither is any request with an Authorization header.

// For those, the browser first sends a PREFLIGHT:
OPTIONS /api/problems
Origin: https://aicancode.org
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

// The server must answer:
Access-Control-Allow-Origin: https://aicancode.org
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400        // cache it, or every call pays twice

// Only then does the real request go. Two round trips per call
// without Max-Age — a real latency cost, not just noise.

Reading the Error

The messages, and what each one actually means.

Four messages, four different causes
// "No 'Access-Control-Allow-Origin' header is present"
//   -> The server did not send the header at all. Either CORS is not
//      configured, or the request 500'd BEFORE the CORS middleware
//      ran. Check the server logs before touching CORS config.

// "The 'Access-Control-Allow-Origin' header has a value
//  'http://localhost:3000' that is not equal to the supplied origin"
//   -> Configured, but for a different origin. Usually the deployed
//      URL was never added.

// "Response to preflight request doesn't pass access control check"
//   -> The OPTIONS request itself failed. Often the route only
//      handles POST, or auth middleware rejected the unauthenticated
//      OPTIONS. Preflights carry no credentials — never require auth
//      on them.

// "Request header field x-request-id is not allowed"
//   -> Add it to Access-Control-Allow-Headers.

// The two conclusions that follow from the mechanism:
//   1. curl succeeding proves nothing — curl has no origin and does
//      not enforce CORS. Only a browser does.
//   2. There is no frontend fix. Not a header you can set, not a
//      fetch option. The server allows it, or it does not.

Key Points to Remember

  • 1An origin is scheme, host and port — a different port is a different origin
  • 2CORS blocks JavaScript from READING a cross-origin response; the request was usually still sent and executed
  • 3POSTing application/json or sending an Authorization header triggers an OPTIONS preflight
  • 4Access-Control-Max-Age caches the preflight — without it every call costs two round trips
  • 5The fix is always on the server, and curl succeeding proves nothing because only browsers enforce CORS

Interview Questions

Sign in to ask Aria
1

What is CORS actually protecting against, and what does it not prevent?

Medium
2

Why does a POST with JSON send an OPTIONS request first?

Medium
3

Your API works in Postman but fails in the browser. What is happening?

Medium

Ask Aria about CORS — What Is Actually Happening

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…