3.3 CORS-safe payloads: recipes that need no server setup
The JSON-as-text/plain envelope, form encoding for flat events, and a unified payload builder that measures what it sends in real bytes.
Intermediate
Knowing the safelist rule is theory; shipping telemetry through it is engineering. This page collects the two payload recipes that put the rule to work — the JSON-as-text/plain trick that needs zero server CORS and the form-encoding shape for flat events — and unifies them in one payload builder. Each recipe runs below exactly as written, with a capture harness standing in for the network so you can watch the decisions happen. Which transport carries the payload is a separate decision, and the hybrid layer later in this series makes it per call.
Recipe 1 — JSON in a text/plain envelope
The derived Content-Type for a string body is
text/plain;charset=UTF-8 — safelisted, so the request stays in no-cors mode and crosses any origin with no CORS configuration at all. Nothing stops the body itself from being JSON. A collector that reads the raw body and parses it as JSON regardless of the Content-Type header accepts these beacons from any origin, forever, with zero preflight traffic. The trade is explicit: the header lies about the body's nature, so anything that routes or validates by Content-Type upstream (API gateways, WAFs, some frameworks' body parsers) must be told to read the body anyway.
// The JSON-as-text/plain send — no CORS setup needed anywhere:
const payload = JSON.stringify({ event: 'pageview', path: location.pathname });
navigator.sendBeacon('https://collector.example.com/ingest', payload);
// derived Content-Type: text/plain;charset=UTF-8 -> no-cors mode
Recipe 2 — form encoding for flat events
When events are key/value shaped,
URLSearchParams is the honest encoding: the platform derives
application/x-www-form-urlencoded;charset=UTF-8 — safelisted, no-cors, and readable by every server-side form parser ever written. It is also the encoding the classic image-pixel analytics era converged on, so migration paths from legacy collectors tend to line up naturally. Its limits are the limits of form encoding: no nesting without conventions (dot-keys, JSON-in-a-field), and URL-encoding overhead on dense payloads.
The payload builder, unifying both
The runnable example implements
buildBeaconPayload(target, value) — one function that returns the data object, the Content-Type the platform will derive, and the mode the request will fly in, for all three targets:
form,
json-text, and
json-blob (the cors-mode one, for same-origin collectors or CORS-configured servers). It also demonstrates the byte-measurement discipline the quota page will demand: sizes measured with
Blob([s]).size in UTF-8 bytes, never
string.length in UTF-16 code units:
Output
Ready to run.
Quick check
Your third-party collector has zero CORS configuration. Which payload reaches it?
navigator.sendBeacon('https://third-party.example.com/ingest', );
Key takeaways
-
String bodies (JSON or otherwise) fly as
text/plain;charset=UTF-8— safelisted, no-cors, zero server setup; the server must parse the body as JSON itself. - URLSearchParams gives standard form-encoding — safelisted and universally parseable, best for flat events.
-
Measure payload sizes with
new Blob([s]).size(UTF-8 bytes), never.length(UTF-16 units) — the quota counts bytes. -
Payload shaping and transport selection stay separate — these recipes fly unchanged on
sendBeacon,fetch(keepalive), orfetchLater. - Separate payload shaping from transport selection; both recipes compose instead of competing.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.