Skip to content

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

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