[Bug]: request interception leaves dedicated-worker fetches paused forever since 24.7.0 (continueRequest sent to the worker session)
Minimal, reproducible example
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.enableon the worker session had already failed with'Fetch.enable' wasn't found. - Its
Network.requestWillBeSentthen arrives on the worker session, where #13752 attached the worker's session to the page's network manager. The resultingHTTPRequestis bound to the worker client, sorequest.continue()sendsFetch.continueRequestthere. - The error is swallowed and
continue()resolves, but the request is never continued.
- The request is paused on the page session.
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 madepage.gototime out on Chromium 142 with site isolation on. So it has to read the internal target info: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
Source: puppeteer/puppeteer