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:
- The
Navigator.sendBeacon()
method, part of the Beacon API, which allows sending an HTTP POST request containing a small amount of data to a web server asynchronously. - The
keepalive
option of the Fetch API, which allows a Fetch request to outlive the page.
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 thewebRequest
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.
- On Firefox, requests sent through the Beacon API are marked with an unambiguous
-
To detect requests sent using the Fetch API with the
keepalive
option enabled, you can just check theirkeepalive: true
attribute:
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);
});
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);
});