Determining if an HTTP request was sent as beacon/keepalive

#javascript, #webdev
Jul 23, 2022

At work, we recently worked on a somewhat unusual integration test: we wanted to determine if a web app was successfully sending a network request after the user left the page.

You might ask: why would you ever need to send a network request after the user leaves the page?

Well, one common use case is to fire and forget some data to a service when you don’t care about the service response; for example, right before the user navigates to another page. In this situation, the browser may be about to unload the page, and in that case, the browser may choose not to send asynchronous network requests.
In the past, web pages have tried to delay the page unloading long enough to send data using ugly workarounds such as submitting the data with a blocking synchronous XMLHttpRequest call.

Luckily, in the last few years, browsers implemented a few ways to send fire-and-forget network requests more reliably. The two most popular ones are:

So, back to our integration test: our goal was to validate that these APIs were correctly sending HTTP requests after the page closed.
The main complexity in doing so was that the same requests we run as fire-and-forget are often fired as classic HTTP requests in the app lifecycle.
So we needed a way to determine whether an HTTP request was sent as beacon/keepalive.

Here’s what we learned:

  • To detect requests sent through the Beacon API, you can use the ResourceType of the webRequest object. ResourceType is a string representing the context in which a resource was fetched in a web request.
    Unfortunately, as of today, the browser compatibility of the available resource types is quite messy:

    • On Firefox, requests sent through the Beacon API are marked with an unambiguous "beacon" resource type.
    • On Chrome & Edge, requests sent through the Beacon API are marked with a more generic "ping" resource type.
    • …and Safari doesn’t support the ResourceType at all, lol.
  • To detect requests sent using the Fetch API with the keepalive option enabled, you can just check their keepalive: true attribute:

keepalive

If you’re interested, here’s what our Playwright integration test looks like (running on Chromium):

import { test, expect } from "@playwright/test";

test("Send an HTTP network request after the user leaves the page ", async ({
  page,
}) => {
  await page.goto("https://localhost");

  let didSendTheFireAndForgetRequest = false;

  page.on("request", (request) => {
    if (didSendTheFireAndForgetRequest) return;
    const resourceType = request.resourceType();
    const isBeaconRequest = ["beacon", "ping"].includes(request.resourceType());
    const isKeepAliveRequest = !!request.keepalive;
    didSendTheFireAndForgetRequest = isBeaconRequest || isKeepAliveRequest;
  });

  // Do something to make the page enqueue the network request...
  await something();

  await page.close({ runBeforeUnload: true });

  await expect
    .poll(() => didSendTheFireAndForgetRequest, { timeout: 10000 })
    .toBe(true);
});