#15509·puppeteer

[Bug]: request interception leaves dedicated-worker fetches paused forever since 24.7.0 (continueRequest sent to the worker session)

Author: alexandrucojocaru-creatopyCreated Sep 28, 2026Updated Sep 28, 2026

Minimal, reproducible example

typescript
import http from 'node:http';
import puppeteer from 'puppeteer';

// A dedicated worker fetches 40 URLs in parallel, then 20 in sequence.
const workerSource = `
self.onmessage = async ({ data: origin }) => {
  let done = 0;
  await Promise.all(Array.from({ length: 40 }, async (_, i) => {
    await (await fetch(origin + '/data?i=' + i)).arrayBuffer();
    done++;
  }));
  for (let i = 0; i < 20; i++) {
    await (await fetch(origin + '/data?s=' + i)).arrayBuffer();
    done++;
  }
  postMessage(done);
};`;

const server = http.createServer((req, res) => {
  if (req.url.startsWith('/data')) {
    res.writeHead(200, { 'content-type': 'application/octet-stream' });
    res.end(Buffer.alloc(4096));
    return;
  }
  res.writeHead(200, { 'content-type': 'text/html' });
  res.end(`<script>
    const source = ${JSON.stringify(workerSource)};
    const worker = new Worker(URL.createObjectURL(new Blob([source], { type: 'text/javascript' })));
    worker.onmessage = (e) => { window.fetchesDone = e.data; };
    worker.postMessage(location.origin);
  </script>`);
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (request) => request.continue());
await page.goto(`http://127.0.0.1:${server.address().port}/`);
try {
  const done = await page.waitForFunction(() => window.fetchesDone, { timeout: 15_000 });
  console.log('worker fetches completed:', await done.jsonValue());
} catch (error) {
  console.log('HANG:', error.message);
}
await browser.close();
server.close();

Background

With request interception enabled, fetch() calls made from a dedicated worker stop completing. They stay paused forever, so the page hangs.

It started in 24.7.0, which added worker network requests to the page's network manager (#13752). In our case a video decoder loads its WASM from a worker, and every render that needed it hung.

Bisect with the script above (Chrome for Testing on macOS; the Chromium builds on Linux; blank = not run):

puppeteer Chrome for Testing 154.0.8037.57 (bundled) Chromium 142.0.7444.59 Chromium 130.0.6723.174
24.4.0 pass pass pass
24.6.1 pass
24.7.0 hang
24.15.0 hang hang hang
25.12.0 hang (3/3) hang hang

Expectation

All 60 worker fetches complete (worker fetches completed: 60), as in 24.6.1 and earlier.

Reality

HANG: Waiting failed: 15000ms exceeded.

This is one request from the run above, with NODE_DEBUG='puppeteer:*'; session ids are replaced by labels:

RECV ◀ {"method":"Fetch.requestPaused","params":{"requestId":"interception-job-3.0","request":{"url":"http://127.0.0.1:PORT/data?i=0",…}},"sessionId":"<PAGE>"}
RECV ◀ {"method":"Network.requestWillBeSent","params":{"request":{"url":"http://127.0.0.1:PORT/data?i=0",…}},"sessionId":"<WORKER>"}
SEND ► {"method":"Fetch.continueRequest","params":{"requestId":"interception-job-3.0"},"id":39,"sessionId":"<WORKER>"}
RECV ◀ {"id":39,"error":{"code":-32601,"message":"'Fetch.continueRequest' wasn't found"},"sessionId":"<WORKER>"}
  • What goes wrong:

    • The request is paused on the page session. Fetch.enable on the worker session had already failed with 'Fetch.enable' wasn't found.
    • Its Network.requestWillBeSent then arrives on the worker session, where #13752 attached the worker's session to the page's network manager. The resulting HTTPRequest is bound to the worker client, so request.continue() sends Fetch.continueRequest there.
    • The error is swallowed and continue() resolves, but the request is never continued.
  • Scale: a single run logged 58 such errors.

  • Order dependence: when the two events arrive in the opposite order, the request is continued on the page session and completes, which is why a single worker fetch usually passes and a burst does not.

  • Workaround: leave dedicated workers unattached. The public target.type() reports both a dedicated worker and an out-of-process iframe as 'other', and filtering on 'other' also detaches OOPIFs, which made page.goto time out on Chromium 142 with site isolation on. So it has to read the internal target info:

    typescript
    const browser = await puppeteer.launch({
      targetFilter: (target) => target._getTargetInfo().type !== 'worker',
    });

    With it, the repro above completes 60/60.

  • Suggestions:

    • Route the continue/respond/abort for a paused request through the session that paused it.
    • Separately, a public way to tell a dedicated worker from an iframe target (e.g. a distinct TargetType) would make the workaround supportable.

Puppeteer configuration file (if used)

No response

Puppeteer version

25.12.0

Node version

24.11.1

Package manager

npm

Package manager version

11.6.2

Operating system

macOS