3.1 Payloads: the BodyInit union and the Content-Type map
Every type sendBeacon's data parameter accepts, the Web IDL stringify truth for odd values, and the fixed Content-Type mapping observed live through Request.
Basic
The second argument of
sendBeacon accepts the Fetch standard's BodyInit union — the same type that
fetch() and
Request accept for bodies. Flattened, the members are:
ReadableStream,
Blob,
BufferSource (an
ArrayBuffer or any typed-array view),
FormData,
URLSearchParams, plain strings — or
null. Note that
URLSearchParams is a first-class member: tutorials claiming sendBeacon cannot take it contradict the official IDL, and the example below constructs one for real.
The stringify truth
What happens to values that are none of those — a number, a boolean, a plain object? Web IDL union conversion has a string catch-all: after every interface member fails to match, a value falls through to the
USVString member and is stringified.
sendBeacon(url, 42) ships the two-character body
"42".
sendBeacon(url, {a: 1}) ships the string
"[object Object]" — silently, with
true returned, because the call is type-valid. Nothing here throws; the platform's contract is conversion, not validation.
You can watch this happen rather than take it on faith. A
Request applies the very same union conversion and body extraction as sendBeacon does, and it lets you read the result back — so the runnable example below hands
Request the odd values and prints the exact body and Content-Type the platform derived from each:
Output
Ready to run.
The lesson hiding in that output is the single most common real-world bug with this API:
a plain object is not JSON. If your collector parses bodies as JSON,
sendBeacon(url, obj) delivers the unparseable string
"[object Object]" with a 200-level queueing result on your side of the fence. Stringify before you send — the example's last line shows the correct form.
One data type, one Content-Type
You never set a Content-Type on a beacon; the extraction algorithm derives it from the data, and the derived value decides the request's CORS mode — the subject of the next page. The mapping is fixed:
Data argument | Derived Content-Type | CORS mode |
|---|---|---|
|
|
|
|
|
|
|
|
|
| the Blob's own
| depends on the type |
| no Content-Type header at all |
|
| no body, no header |
|
| never happens —
| — |
The three form-family defaults are the quiet heroes of cross-origin beacons: all three are CORS-safelisted Content-Types, so those requests stay in no-cors mode and need no CORS setup whatsoever on the server. A Blob typed
application/json breaks that streak — the next page shows why that single header value restructures the entire request.
Output
Ready to run.
Quick check
Your collector expects JSON. Which call actually ships JSON?
navigator.sendBeacon('/api/collect', );
Key takeaways
-
data accepts the full
BodyInitunion — includingURLSearchParams, which some tutorials wrongly deny. -
Odd values do not throw; Web IDL stringifies them.
{a: 1}ships as"[object Object]". -
Always
JSON.stringify()objects before sending — the platform will not do it for you. -
The Content-Type is derived from the data: string →
text/plain;charset=UTF-8, form → form encodings,BufferSource→ none. - The derived Content-Type decides no-cors versus cors mode — the mechanism the next page dissects.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.