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

7.1 Service workers and beacons: detection and safe forwarding

The keepalive-POST-empty-destination detection triple with its honest false positives, and the buffer-then-re-issue patterns that never lose data.

Levels: Advanced ~18 min read

Advanced

Beacons are ordinary fetches from the network's point of view, and that means a service worker's fetch event sees them — a page's telemetry passes through the same interceptor as its API calls. Interception is powerful (offline buffering, rewriting, deduplication, absorption) and dangerous in exact proportion, because the beacon contract gives the page no feedback loop: a service worker that mishandles a beacon loses data invisibly, with true already returned upstream.

What the worker sees

Inside the handler, event.request is a Request object shaped by the fixed recipe you met in part one: method POST, keepalive true, mode no-cors (for safelisted payloads), credentials include — and destination the empty string, because beacons load nothing. What the worker cannot see is the initiator: the Fetch standard tracks an initiator type per request — and "beacon" is literally one of its values — but that field is internal bookkeeping for Resource Timing, CSP reports, and DevTools; the Request class exposes no initiator property, and Resource Timing's initiatorType is not readable from inside the worker's fetch event either.

The detection triple and its honest false positives

The best JS-visible signature is the triple: keepalive === true && method === 'POST' && destination === ''. The runnable example builds real Request objects — including an actual beacon-shaped one, constructible because keepalive is a settable RequestInit member — and walks the predicate across every interesting shape, false positives included:

Interactive Runnable example
Edit

Ready to run.

The one ambiguous row is the honest cost of the design: a hand-rolled fetch(url, {method:'POST', keepalive:true}) — the worker-side substitute from earlier in this series — is indistinguishable from a beacon, because a beacon is that request. When the predicate gates destructive behavior (absorbing or rewriting), scope it by URL pathname as the example's second function does; when it merely logs, the triple alone is fine. The false-positive surface is bounded, known, and permanent — the platform chose not to expose initiator, and code that plans around that fact stays correct.

Forwarding: the patterns that do not lose data

The dangerous moment is forwarding, and one spec fact writes all the rules: event.request.body is a ReadableStream, and streams throw TypeError under keepalive. So the intuitive forwarding — fetch(url, {method: 'POST', body: event.request.body, keepalive: true}) — is broken by construction. The second trap is timing: respondWith must be called synchronously in the handler; awaiting before it can end the event before your response exists. The safe core is buffer-then-re-issue, and it runs below against real Request objects exactly as the worker would handle them:

Interactive Runnable example
Edit

Ready to run.

Three shipping patterns grow from that core. Pass-through: event.respondWith(fetch(event.request)) forwards the original untouched — no body read, no stream problem, keepalive semantics preserved by the platform. Buffered re-issue: reissueKeepalive(event.request, overrides) clones, reads the body to a string, and constructs a fresh keepalive request — the modify/redirect path, at the cost of materializing the body (bounded by the 64 KiB quota anyway). Short-circuit: event.respondWith(new Response(null, {status: 204})) absorbs the beacon and answers it locally — the spec's own encouraged response shape, from inside the worker. Each pattern is one line of handler code around the tested core; the example's closing comment shows the wiring.

Quick check

Which forwarding call is spec-safe inside a fetch handler?

self.addEventListener('fetch', (event) => {
  event.respondWith();
});

Key takeaways

  • Beacons hit the fetch event like any request: POST, keepalive, no-cors, empty destination — but no JS-visible initiator.
  • Detect with keepalive && POST && destination === ''; the hand-rolled keepalive fetch is a permanent, known false positive — scope by URL when it matters.
  • Never forward event.request.body with keepalive: streams throw TypeError; buffer via clone().text() and re-issue.
  • Call respondWith synchronously; async work happens inside the promise you hand it.
  • Short-circuit with 204 — the spec's own encouraged response — and treat intercepted beacons as data you now own.
Was this page helpful?

Mark this learning resource complete to track your learning progress.