Why did the browser block that fetch?
The network tab shows a 200. Your code sees an error. Both are correct, and the difference is the whole security model.
- Level
- SYSTEMS
- Read time
- 11 min
- Experiment
- Available
- Type
- INTERACTIVE
The question
A cross-origin fetch returns a CORS error in the console, yet the server logged the request and returned data. What exactly did the browser stop?
Hypothesis
The same-origin policy restricts what a document may *read*, not what it may send. The request was made; the response was withheld from your script.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryPick a target, then change what the server sends back and what the request asks for. The bench reports which operations the browser permits and why.
What we observed
Sending is always allowed. Reading almost never is, unless the server explicitly opts the caller in. That asymmetry is deliberate: the browser cannot know whether a request is harmful, but it can stop one site from harvesting another site's responses.
| Pair | Same origin | Same site |
|---|---|---|
| https://a.example.com and https://a.example.com | yes | yes |
| https://a.example.com and https://b.example.com | no | yes |
| https://example.com and http://example.com | no | no (scheme counts) |
| https://example.com and https://example.com:8443 | no | yes |
| https://example.com and https://partner.test | no | no |
Why it happens
CORS is not a restriction added on top of the web. It is the mechanism that *relaxes* an older restriction. Before it existed, a cross-origin response was simply unreadable; CORS gives the server a way to say which origins may read what.
HTTP/1.1 200 OKContent-Type: application/jsonAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Credentials: trueVary: OriginA preflight appears when the request is not simple: a method other than GET, HEAD or POST, a Content-Type outside the three form-ish values, or custom headers. The browser sends OPTIONS first and refuses to send the real request until the server approves it.
Why can a page load an image from anywhere but not read a JSON response?
Because embedding a resource and reading its bytes are different capabilities.
Further research
Cookies have their own axis. SameSite=Lax is the modern default and withholds cookies on cross-site subresource requests while still sending them on top-level navigations. SameSite=None restores the old behaviour but requires Secure, and is increasingly restricted by browsers phasing out third-party cookies.
References
- 01MDNSame-origin policy
- 02WHATWGFetch Standard, CORS protocol
- 03web.devSameSite cookies explained