Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~13 min read

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.

js
// 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:

Interactive Runnable example
Edit

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), or fetchLater.
  • Separate payload shaping from transport selection; both recipes compose instead of competing.
Was this page helpful?

Mark this learning resource complete to track your learning progress.