[Bug]: page.goto() ignores its own timeout and never settles after a second navigation request
Minimal, reproducible example
import http from 'node:http';
import puppeteer from 'puppeteer';
// '/start' loads fine and then navigates again from its own load handler.
// '/never' is accepted and never answered.
const server = http.createServer((req, res) => {
if (req.url === '/start') {
res.writeHead(200, {'content-type': 'text/html'});
res.end(`<html><body onload="location.href='/never'">ok</body></html>`);
}
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const {port} = server.address() as {port: number};
const browser = await puppeteer.launch({args: ['--no-sandbox']}); // container
const page = await browser.newPage();
const startedAt = Date.now();
setTimeout(() => {
console.error(`still pending after ${Date.now() - startedAt}ms, timeout was 3000ms`);
process.exit(1);
}, 15_000).unref();
await page.goto(`http://127.0.0.1:${port}/start`, {timeout: 3000});
console.log(`settled after ${Date.now() - startedAt}ms`);
await browser.close();
server.close();Background
Hit in a test suite. A page.goto() that never settles is invisible from the outside — no error,
no stack, nothing to grep for; the process simply stops making progress.
Reproduced on Debian 12 (bookworm) x86_64 in a Docker container, kernel 7.0.0-31-generic, both
headless and headful (headless: false under xvfb-run), on the bundled Chrome of each version —
138.0.7204.49 with puppeteer 24.11.1 and 153.0.8010.36 with 25.11.0.
Expectation
TimeoutError: Navigation timeout of 3000 ms exceeded after ~3s.
GoToOptions extends WaitForOptions, whose timeout is documented as "Maximum wait time in
milliseconds. Pass 0 to disable the timeout."
Reality
still pending after 15007ms, timeout was 3000msThe promise never settles — the 15s cap above is just where I stop waiting.
page.waitForNavigation() on the same server, with the same 3000ms, behaves correctly:
puppeteer 24.11.1
page.goto(timeout:3000) -> still pending 15008ms
page.waitForNavigation(timeout:3000) -> TimeoutError 3002ms
puppeteer 25.11.0
page.goto(timeout:3000) -> still pending 15006ms
page.waitForNavigation(timeout:3000) -> TimeoutError 3001msWhere it comes from
packages/puppeteer-core/src/cdp/Frame.ts — goto ends on a bare await, waitForNavigation
races the same value against the termination promise:
// goto
return await watcher.navigationResponse();
// waitForNavigation
const result = await Deferred.race<Error | HTTPResponse | null | undefined>([
watcher.terminationPromise(),
watcher.navigationResponse(),
]);packages/puppeteer-core/src/cdp/LifecycleWatcher.ts — the awaited deferred carries no timeout of
its own, and is replaced on every navigation request reported for the frame:
#onRequest(request: HTTPRequest): void {
if (request.frame() !== this.#frame || !request.isNavigationRequest()) return;
this.#navigationRequest = request;
this.#navigationResponseReceived?.resolve();
this.#navigationResponseReceived = Deferred.create(); // no timeout
if (request.response() !== null) this.#navigationResponseReceived?.resolve();
}So once the document's lifecycle completes, a further navigation request replaces
#navigationResponseReceived with a fresh deferred. If that request never gets a response or a
failure, goto waits on it forever, and terminationPromise() — the one carrying the navigation
timeout — is no longer in the race.
Giving goto the same guard waitForNavigation already has looks like it would be enough:
return await Deferred.race([watcher.terminationPromise(), watcher.navigationResponse()]);Puppeteer version
25.11.0 (also reproduces on 24.11.1)
Node version
22.23.2
Package manager
npm
Package manager version
10.9.8
Operating system
Linux
Source: puppeteer/puppeteer