Skip to content

[Bug]: API calls never settle on an unresponsive page when tracing snapshots are on #42903

Closed

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 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)

Activity

  1. ashrafiucse commented on Sep 24, 2026

    @ashrafiucse
    Contributor

    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.

  2. DepravityDemo commented on Sep 25, 2026

    @DepravityDemo
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions