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
main in 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.ts and playwright.trace.spec.ts still pass.
eslint and tsc are 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)
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 owntimeoutapplies.Expected behavior
Same as with
snapshots: false. Tracing should not turn a bounded call into an unbounded one:Actual behavior
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
DispatcherConnectionrunsonBeforeCall/onAfterCallunder a 3000msProgressController.ProgressControllercan only interrupt awaits that go throughprogress.race().Tracing._captureSnapshotawaits the DOM snapshot directly: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-progresslint rule exists to catch exactly this. It misses this line because of the optional chaining:?.turns the expression into aChainExpression, and the rule skips those. With?.replaced by!., eslint reportsprogress/await-must-use-progresson this line.Proposed fix
Race the snapshot against
progress, the same way_captureCoveragein the same file already does:With this change, the repro above gives
evaluate resolved after 3005msandgoto 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"):mainin chromium, firefox and webkit.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