Repository navigation
[Bug]: API calls never settle on an unresponsive page when tracing snapshots are on #42903
Description
Version
1.63.0 (also reproduced on main @ b7957c5)
Steps to reproduce
With tracing.start({ snapshots: true }), once the page's main thread is busy, API calls on that page never settle: not even their own timeout applies.
const http = require('http');
const { chromium } = require('playwright');
const settles = (p, ms) => Promise.race([
p.then(() => 'resolved', e => `rejected: ${e.message.split('\n')[0]}`),
new Promise(r => setTimeout(() => r('NEVER SETTLED'), ms)),
]);
(async () => {
const server = http.createServer((req, res) => res.end('<title>ok</title>'));
await new Promise(r => server.listen(0, r));
const url = `http://127.0.0.1:${server.address().port}/`;
for (const snapshots of [false, true]) {
const browser = await chromium.launch();
const context = await browser.newContext();
await context.tracing.start({ snapshots });
const page = await context.newPage();
await page.goto(url);
// Make the page's main thread busy right after this call returns.
let start = Date.now();
let outcome = await settles(page.evaluate(() => { setTimeout(() => { while (true) {} }, 0); }), 20000);
console.log(`snapshots=${snapshots}: evaluate ${outcome} after ${Date.now() - start}ms`);
start = Date.now();
outcome = await settles(page.goto(url, { timeout: 3000 }), 20000);
console.log(`snapshots=${snapshots}: goto ${outcome} after ${Date.now() - start}ms`);
await settles(browser.close(), 5000);
}
process.exit(0);
})();Expected behavior
Same as with snapshots: false. Tracing should not turn a bounded call into an unbounded one:
snapshots=false: evaluate resolved after 14ms
snapshots=false: goto rejected: page.goto: Timeout 3000ms exceeded. after 3005ms
Actual behavior
snapshots=true: evaluate NEVER SETTLED after 20002ms
snapshots=true: goto NEVER SETTLED after 20002ms
This happened on every run: 3/3 on macOS 26 arm64, and it also reproduces on Ubuntu (Linux 5.15 x64). It also reproduces through playwright-python 1.63.0 with the sync API, where the process just sits idle forever. In our CI, page.goto(timeout=30000) hung this way for 3+ days in several processes on a heavy page.
Root cause
DispatcherConnection runs onBeforeCall/onAfterCall under a 3000ms ProgressController. ProgressController can only interrupt awaits that go through progress.race(). Tracing._captureSnapshot awaits the DOM snapshot directly:
await this._snapshotter?.captureSnapshot(page, progress.metadata.id, phase, resetTargets).catch(() => {});Snapshotter.captureSnapshot → frame.nonStallingRawEvaluateInExistingMainContext() only races against dialogs and navigations. It does not race against a timeout. When the page never answers the evaluation, the before/after hook never finishes, and so the call never gets a response.
The progress/await-must-use-progress lint rule exists to catch exactly this. It misses this line because of the optional chaining: ?. turns the expression into a ChainExpression, and the rule skips those. With ?. replaced by !., eslint reports progress/await-must-use-progress on this line.
Proposed fix
Race the snapshot against progress, the same way _captureCoverage in the same file already does:
try {
await progress.race(this._snapshotter.captureSnapshot(page, progress.metadata.id, phase, resetTargets));
} catch {
}With this change, the repro above gives evaluate resolved after 3005ms and goto rejected: Timeout 3000ms exceeded after 6004ms: the call finishes, and the snapshot hooks can only add their 3s budget. Snapshots of healthy pages are unaffected.
I have the patch ready with a regression test in tests/library/tracing.spec.ts ("should not hang when page is unresponsive"):
- The test fails on
mainin chromium, firefox and webkit. - With the fix it passes in all three, 5/5 repeats each.
tracing.spec.ts,trace-viewer.spec.ts,snapshot-renderer.spec.tsandplaywright.trace.spec.tsstill pass.eslintandtscare clean.
I'd be happy to open a PR if this approach looks right to you.
Environment
System:
OS: macOS 26.6.2, CPU: arm64 Apple M5
Also: Ubuntu, Linux 5.15.0-118-generic x64
Binaries:
Node: 25.2.1 (macOS), 22.19.0 (Linux)
npmPackages:
playwright: 1.63.0 (also playwright-python 1.63.0)
Activity
ashrafiucse commentedon Sep 24, 2026
I've opened #42907 with this fix plus a regression test (hangs into the 30s timeout on main, settles in all three browsers with the change). The approach is exactly the one you proposed - happy to close it and review yours instead if you'd rather send the PR yourself.
DepravityDemo commentedon Sep 25, 2026
Thanks Yury Semikhatsky (@yury-s) for the quick fix in #42919, and for extending await-must-use-progress to cover optional chaining. That should help catch similar cases going forward.
Thanks also Ashraf Ali (@ashrafiucse) for putting together #42907 and referencing the analysis. Much appreciated.
I’ll verify once it ships in a release. Until then, we’ll keep our client-side timeout guard in place.