Skip to content

[Bug]: connectOverCDP still hangs when an existing page's renderer is alive but busy (follow-up to #42936) #44052

Open

Description

Version

1.65.0

Steps to reproduce

Minimal repro repo: https://github.com/caihongdepiaoyi/pw-busy-renderer-repro

  1. Clone the repo and npm install (deps: playwright-core@1.65.0 only)
  2. node repro-busy-renderer.mjs — launches Chromium with --remote-debugging-port=9333, opens a page whose renderer runs an endless synchronous loop (for(;;)) after 100 ms, then attempts a second connectOverCDP() with timeout: 10000 → TimeoutError after ~10000 ms
  3. node repro-crashed-page-control.mjs (control case) — same setup, but the pre-existing page's renderer is crashed via Page.crash instead of busy → CONNECTED in ~10 ms (proves the fix(chromium): do not hang on pages without a renderer when connecting over CDP #42936 fix is active for its scope)

Core of the busy repro (full files in the repo):

import { chromium } from 'playwright-core';
const PORT = 9333;
const browser = await chromium.launch({ args: [`--remote-debugging-port=${PORT}`] });
const user = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`);
const page = await user.contexts()[0].newPage();
await page.goto('data:text/html,<script>setTimeout(()=>{for(;;){}},100)</script>');
await new Promise(r => setTimeout(r, 600));

const t0 = Date.now();
try {
  const second = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`, { timeout: 10000 });
  console.log('CONNECTED in', Date.now() - t0, 'ms');
  await second.close();
} catch (e) {
  console.log(e.constructor.name, 'after', Date.now() - t0, 'ms');
}
await browser.close();

Expected behavior

connectOverCDP should not be held hostage by a page that is neither crashed nor discarded — merely busy. A page that answers nothing within some bounded window during connect could either:

  • be left to finish initializing in the background (it would still be reported via the regular page event once it responds), or
  • cause connect to fail fast with a clear error listing the offending target.

Either way, one busy tab out of N should not block attaching to the browser until the overall timeout expires.

Actual behavior

On playwright-core 1.65.0 (verified that the #42936 fix is included and works for its scope), connectOverCDP() still hangs indefinitely when a pre-existing page's renderer is alive but its main thread is blocked by a long synchronous JavaScript task.

CRBrowser.connect awaits _waitForAllPagesToBeInitialized() — an unbounded Promise.all over every pre-existing page. A busy renderer never crashes, so Inspector.targetCrashed never fires and Inspector.enable itself is never answered, so one busy tab stalls the whole connect until its long task finishes (verified: a connection started mid-task resolves after ≈ the task's remaining runtime).

A/B on the same Chromium build, same port, same playwright 1.65.0:

pre-existing page state second connectOverCDP notes
renderer crashed via Page.crash (not awaited) CONNECTED in 10 ms #42936 fix is active ✔️
renderer busy in for(;;) TimeoutError after 10003 ms still hangs ❌

During the hang, GET /json/version answers in 3 ms and a raw page-level WebSocket to a healthy page answers in a few ms — the DevTools HTTP/WS layers are fine; only the commands sent to the busy page's renderer (Page.enable, Runtime.enable, Inspector.enable, …) never return, and the overall timeout eventually fires: TimeoutError after ~10000 ms.

Additional context

Why this matters. Real-world triggers include a heavy synchronous render, a long GC pause, a page stuck in a long loop, or simply a heavy page being loaded — no crash is involved, and when the task eventually finishes, the page is perfectly healthy again. Clients that attach to long-lived browsers with many tabs (one blocked tab out of N) fail to connect at all, and retrying is ineffective because every retry runs into the same unbounded wait. #42730 and #42882 reported the same symptom and both were closed as duplicates (Sep 21 / Sep 28) before #42936 merged (Sep 30); #42936's mechanism (targetCrashed reporting) structurally cannot cover the alive-but-busy case.

Why this is not a duplicate of #42730 / #42882. Both report this exact symptom — #42882's repro is literally the same busy-renderer script as the one above — and both are closed: #42730 as a duplicate on Sep 21, #42882 (along with its unmerged fix PR #42883, a bounded-grace-period approach that would have covered this case) on Sep 28 — both before #42936 merged on Sep 30. #42936 is the only fix that ever shipped, and it covers crashed/discarded targets only. The A/B above on 1.65.0 (latest stable) shows the alive-but-busy case still hangs, and no open issue tracks it — hence this follow-up. Happy for it to be folded into a reopened #42730/#42882 or a tracking issue instead, whichever maintainers prefer.

Root cause. Unchanged since #41093/#42882: in CRBrowser.connect() (persistent-context branch taken by connectOverCDP), the final step is await browser._waitForAllPagesToBeInitialized() with no per-page bound. One possible direction is the one explored in #42883 (give a page a bounded grace period during connect, then let it initialize in the background and re-apply any context-level updates it missed); happy to help resurrect that approach in whatever shape maintainers prefer.

Environment

System:
  OS: macOS 26.2
  CPU: (14) arm64 Apple M4 Pro
  Memory: 762.59 MB / 24.00 GB
Binaries:
  Node: 24.18.0
  npm: 11.16.0
npmPackages:
  playwright-core: ^1.65.0 => 1.65.0

Activity

  1. microsoft-playwright-automation commented on Nov 11, 2026

    @microsoft-playwright-automation
  2. microsoft-playwright-automation commented on Nov 11, 2026

    @microsoft-playwright-automation
    Contributor

    Hi, I'm the Playwright bot. I reproduced this and checked an early-return change.

    Reproduced on current main (1.66.0-next, 3b438229c6a2ccdbe2d3572755935892203d83c3), Chromium 158.0.8127.18 on macOS, headed and headless. A busy renderer still blocks attachment; noDefaults does not help. This case is not fixed by #42936.

    I have not proposed a fix. Returning early changes which existing pages are available after connection, and simply bounding the wait loses context updates: in an experiment, an init script added after connection never reached the delayed page, even after it recovered and navigated again. This needs an explicit partial-readiness API policy, rather than a timeout around the existing wait. I recommend an opt-in mode that preserves today's complete initial page list by default and reconciles context updates before exposing delayed pages. The compatibility concern was also recorded in the review of #41128.

    Reproduction and results

    This fixture-based test fails at connectOverCDP, after the WebSocket connects. The console event confirms that the loop has started, without a timing guess.

    test('attach while an existing renderer is busy', {
      annotation: {
        type: 'issue',
        description: 'https://github.com/microsoft/playwright/issues/44052',
      },
    }, async ({ browserType, launchPersistent, server }, testInfo) => {
      test.fail(true, 'Current connection waits for every existing page.');
      const port = 9339 + testInfo.workerIndex;
      const { context, page } = await launchPersistent({
        args: [`--remote-debugging-port=${port}`, '--site-per-process'],
      });
      await page.goto(server.EMPTY_PAGE);
      const healthy = await context.newPage();
      await healthy.goto(server.CROSS_PROCESS_PREFIX + '/title.html');
      const started = page.waitForEvent('console', m => m.text() === 'busy');
      void page.evaluate(() => {
        console.log('busy');
        for (;;) {}
      }).catch(() => {});
      await started;
      const connected = await browserType.connectOverCDP(
        `http://127.0.0.1:${port}`, { timeout: 2000 });
      expect(await connected.contexts()[0].pages()[0].title()).toBe('Woof-Woof');
      await connected.close();
    });
    Case Headless Headed
    Six-second loop, 1-second connection timeout, with and without noDefaults Both time out Both time out
    Connect again during that finite loop with a 10-second timeout Connects when loop ends, about 6 seconds after loop start; both pages present Same
    Endless loop, 1-second connection timeout, with and without noDefaults Both time out Both time out
    Healthy page in another renderer during failed connection attempts Title remains readable Same
    HTTP /json/version during finite-loop connection failures Responds Responds
    Desired early attachment, 2-second timeout Fails with TimeoutError Same

    Additional checks on unchanged main:

    • Existing crashed-page, crashed-opener and complete-page-list tests pass.
    • The discarded-page test timed out in the first grouped run; an isolated rerun with API logging passed, including attachment after discard.
    • Context init-script propagation after a finite busy period passes.

    The temporary early-return experiment raced initialization against 500 ms. It returned only the healthy page. After context.addInitScript('window.connectionMarker = "installed"'), the busy page eventually appeared, but the marker remained undefined after navigation. Without that experiment, the marker was "installed". The experimental source change was reverted.

    Commands used: npm run build; npm run ctest -- tests/library/chromium/connect-over-cdp-busy.spec.ts --workers=1 --retries=0 --reporter=line (also --headed); the context-init-script case separately on main and the experiment; and connect-over-cdp.spec.ts --grep 'CD-00[234]|CD-017', with CD-004 rerun under DEBUG=pw:api.

    Only current main was tested, not the reported 1.65.0 package. Firefox and WebKit do not support connectOverCDP, so they are not applicable. No public API or default behavior was changed.

    Triaged on macOS by the Playwright bot - agent run

  3. vaidehi-snk commented on Nov 11, 2026

    @vaidehi-snk

    I’d like to work on this issue if community contributions are welcome. The busy-renderer repro is clear, and I’ll make sure any fix keeps healthy pages connectable while preserving the existing page initialization and context-update behavior. Could a maintainer confirm the approach and assign the issue to me before I start?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions