Why does a cross-origin request the browser blocks reach the calling script with no status code and no body?
answer
- two events, one response
- the server logged it; script did not
- the check runs after the bytes arrive
- network error, not an HTTP error
- status 0, empty headers, null body
basics
~20 sA failed CORS check replaces the server's response with a network error before script sees it - type "error", status 0, an empty header list, a null body - so the call rejects with a bare TypeError that carries no protocol detail at all.
solid answer
~40 sTwo separate things happen. The server received the request, produced a response and logged it; the browser then ran the **CORS check** on a response it already held in full, found no grant naming the calling origin, and refused to hand it over. What it substitutes is a **network error**: response type `"error"`, status `0`, an empty header list and a null body. A call that lands on one does not resolve with something to inspect - it rejects, and the rejection is a bare `TypeError`. That opacity is deliberate: releasing the status or the headers would leak precisely the cross-origin information the check exists to withhold. The practical consequence is that the error object is not evidence, and the two wire exchanges are.
code
http · 10 linesGET /picks/queue?station=17 HTTP/1.1
Host: api.example.net
Origin: https://console.example.net
Accept: */*
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 47
{"station":17,"picks":[{"bin":"A-14","qty":3}]}go deeper
Recall the shape: a blocked cross-origin read becomes a network error with status 0, no headers and no body, surfaced as a bare TypeError. Knowing it carries nothing is most of the answer.
Explain the ordering - the response arrived in full and was logged, then the check ran and the browser substituted the network error. Name which side enforces and which side grants.
Show that you treat the error object as worthless evidence and go straight to the two wire exchanges and the server's own log, rather than instrumenting the calling code further.
Frame the cost: teams burn days here because the failure is designed to be uninformative, so the leverage is in making the grant a reviewable property of each service rather than a thing rediscovered per incident.
## Two different things happened to one response A cross-origin call that "fails" has usually not failed once. Two independent things happened, and keeping them apart is the whole of this leaf: 1. **The server received a request, produced a response, and logged it.** Nothing about the caller being on another origin changes that. The request travelled, the handler ran, the bytes came back, and the access log recorded whatever status the handler produced - very often `200`. 2. **The browser then ran the CORS check on a response it had already received in full.** The check asks one question: does this response carry a grant naming the origin that called? If it does not, the browser does not hand the response to the script. The direction matters and is the most common thing stated backwards. The **browser enforces**; the **server only grants**. A server does not block a cross-origin read - it simply fails to state that one is permitted, and the browser withholds what it is already holding. ## What the script is actually handed When the check fails, the browser does not pass the response through with a marker on it. It substitutes a **network error**, which is a response with a fixed and deliberately empty shape: - its **type** is `"error"`; - its **status** is `0`, because no status line survived into it; - its **header list** is **empty** - not one response header is readable, not even the ones script may normally read; - its **body** is **null**. A call that lands on a network error does not resolve with an object to inspect. It **rejects**, and what it rejects with is a bare `TypeError`: an error object carrying a message and no protocol data whatsoever. There is no status on it, no header collection, no payload, and no field naming which rule was violated. ## An HTTP error you can read versus a failure you cannot | what arrived | status visible to script | headers | body | can script branch on it? | |---|---|---|---|---| | an HTTP error the server returned **and** the browser released | the real one, e.g. `403` | readable | readable | yes | | a network error from a failed CORS check | `0` | empty | null | no | The asymmetry surprises people the first time: a `403` the browser is permitted to release is far more informative than a `200` it is not. "The server returned an error" and "the browser withheld a success" look identical from the calling code, and only the second one leaves a clean green line in the server's log. ## Why the browser refuses to say more The opacity is the feature, not an oversight. The rule the check enforces is about **reading**, not about sending: a cross-origin request has always been allowed to leave, and what is controlled is whether the answer may be read. If the failure told the caller the status, a hostile page could learn from the status alone whether an internal host exists, whether a path is present, or whether the visitor is signed in somewhere - all without ever reading a byte of the body. Handing back a detailed error would re-open the exact channel the check closes, so the failure is flattened to one indistinguishable shape. ## What this means when you are diagnosing - **Treat the error object as "did not complete readably" and nothing more.** It cannot tell you whether the host resolved, whether the connection was refused, whether the answer was `200` or `500`, or which condition failed. - **The server's access log is one half of the evidence.** A green `200` there is entirely compatible with the page reading nothing at all. - **The wire exchange is the other half**, and it is the only half that shows the response headers - which is where the grant either is or is not. - **The fix lives in the response.** A grant is something a server states about a caller's origin; it is not something a caller can assert about itself, so no amount of work in the error handler produces one. - **Resist the first reflex on the server, too**, which is to widen the grant to the broadest possible value. That is a decision about who may read the API, and it does not belong in a debugging session. ## The shape of a real investigation Because the error carries nothing, the investigation has to move to where evidence exists: reproduce the exchange outside the browser, look at the response headers the server really emits for that URL, and check them against the origin the page really sends. The error object never becomes more informative than it was in the first second.
- Why is a 403 the browser releases more useful to you than a 200 it withholds?Because the `403` arrives intact: its status line and headers are readable, so the calling code can branch on it and a human can see what the server decided. The withheld `200` is flattened into a network error with status `0` and an empty header list, which says only that nothing readable arrived.
- If the rejection carries nothing, where can a fix for this even live?In the response. A grant is a statement a server makes about the calling origin, and it travels as a response header on the exchange the browser judges. That is why diagnosis moves to the two wire exchanges rather than to the error handler: the error handler will never have more to work with than it does now.
A parcel is delivered to the building and signed for at the door. The doorman then decides you are not on the list for it and destroys it, telling you only that nothing came - while the sender's records still show a completed delivery.
saying these in an interview costs you the question
- Says the server refused the request because of CORS.
- Expects the rejection object to carry the response status code.
- Reads status 0 as proof the host was unreachable.
- Assumes an empty body means the server returned nothing.
- Treats the error object as diagnostic evidence about the server.