3.2 The CORS trap: no-cors by default, silent drops always
The safelist rule that decides a beacon's CORS mode, what opaque responses cost, why refused preflights are invisible, and the JSON-Blob story sorted from folklore.
Intermediate
Beacons cross origins constantly — that is their job. And the single decision that shapes every cross-origin beacon is one you never make directly: the request's CORS mode. The Beacon processing model starts every request in
no-cors mode. The moment the derived Content-Type is not CORS-safelisted, it flips the request to
cors mode. Everything about failure behavior follows from which side of that line your payload lands on.
The mode switch, from the spec's own steps
The processing model runs, in order: if data was given, extract it (deriving the Content-Type); if a Content-Type was derived, set
corsMode to
"cors"; then, if that Content-Type value is a CORS-safelisted request-header value, set
corsMode back to
"no-cors". The test for Content-Type, in plain language: the value must be at most 128 bytes, contain no CORS-unsafe bytes, parse as a MIME type, and its
essence — the type/subtype pair, with parameters stripped — must be one of
application/x-www-form-urlencoded,
multipart/form-data, or
text/plain.
The runnable example implements the safelist test exactly as the spec defines it and walks each interesting value through the mode decision — including uppercase types (the MIME parse lowercases them) and the length gate:
Output
Ready to run.
What no-cors actually means — and why failures are silent
In no-cors mode the request goes out as a simple form-style POST, and the response it gets back is an opaque filtered response — status 0, no headers, no body, unreadable by JavaScript. That is the trade the mode makes: you get to send cross-origin without any server CORS setup, and in exchange you can never learn anything about what came back. A beacon was never going to read its response anyway, so for telemetry this is usually the right trade.
The same opacity is what makes
cors-mode failures invisible. When a payload flips the request into cors mode — a JSON-typed Blob, most commonly — the browser first performs a CORS preflight. If the server does not answer the preflight with the right headers, the request is dropped before any POST happens. Your code sees nothing: no exception, no rejection, no callback.
sendBeacon returned
true back when the data was queued, and that is the last you hear of it. Silent drops are not a bug in the API; they are the documented consequence of a fire-and-forget contract meeting an unconfigured server.
The JSON Blob story, told correctly
Here is where a decade of tutorial folklore needs careful sorting. You will read that sending a Blob typed
application/json throws a
TypeError in some browsers, with a specific version cited. What the
specification says is unambiguous and different: the Blob is accepted, its Content-Type is not safelisted, the request switches to cors mode, a preflight is issued, and — if the server answers it — the POST proceeds. Individual browsers have at times thrown instead of proceeding, and those behaviors were real in those builds; but they are engine behaviors, not spec facts, they contradict each other across sources, and this series does not teach them as rules.
What you can rely on, always, is the decision procedure this page has already given you. Safelisted Content-Type → no-cors, no server setup needed, failures silent and rare. Non-safelisted → cors mode, preflight required, failures silent and certain if the server is not ready. If you want JSON semantics without the CORS machinery, the next page shows the two payload shapes that achieve exactly that.
Quick check
A Blob with type 'application/json' flips the beacon into which mode?
const blob = new Blob([json], { type: 'application/json' });
navigator.sendBeacon(url, blob);
// request mode =
Key takeaways
-
Every beacon starts in
no-cors; a non-safelisted derived Content-Type flips it tocors. -
Safelisted = ≤128 bytes, safe bytes, and essence
text/plain/x-www-form-urlencoded/multipart/form-data— parameters ignored. - no-cors responses are opaque: status 0, no headers, unreadable — the price of zero server setup.
- A refused preflight drops the beacon silently; the boolean stays true either way. Verify important metrics server-side.
- Blob-with-JSON throwing is engine folklore, not spec behavior; the spec's path is: accept, switch mode, preflight.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.