Skip to content

[Bug]: page.close() hangs forever — Chromium acks Target.closeTarget with success:true without closing a target mid-commit in a meta-refresh navigation #42366

Closed as duplicate

Description

@wittjeff

Version

1.62.1

Steps to reproduce

Run the self-contained script below (node repro.js, only dependency is playwright). It serves two local pages, one of which client-side redirects immediately via <meta http-equiv="refresh" content="0; URL='/target.html'"> (the W3C ACT-Rules bc659a "passed" fixtures are real-world examples of such pages), then loops: goto → evaluate (destroyed by the refresh navigation — expected) → page.close().

const http = require('http');
const {chromium} = require('playwright');

const pages = {
  '/a.html': `<!DOCTYPE html><html lang="en"><head><title>a</title>
    <meta http-equiv="refresh" content="0; URL='/target.html'"></head>
    <body><p>redirecting</p></body></html>`,
  '/target.html': `<!DOCTYPE html><html lang="en"><head><title>t</title></head>
    <body><p>arrived</p></body></html>`
};

const settles = (promise, ms) => Promise.race([
  promise.then(() => 'resolved').catch(() => 'rejected'),
  new Promise(resolve => setTimeout(() => resolve('NEVER SETTLED'), ms))
]);

(async () => {
  const server = http.createServer((req, res) => {
    res.setHeader('content-type', 'text/html');
    res.end(pages[req.url] || pages['/target.html']);
  });
  await new Promise(resolve => server.listen(0, resolve));
  const base = `http://127.0.0.1:${server.address().port}`;
  const browser = await chromium.launch();
  const context = await browser.newContext();
  let hangs = 0;
  for (let round = 0; round < 10; round++) {
    const page = await context.newPage();
    await page.goto(`${base}/a.html`, {waitUntil: 'load'});
    await page.evaluate(
      () => new Promise(resolve => setTimeout(resolve, 100))
    ).catch(() => {});
    const outcome = await settles(page.close(), 10000);
    console.log(`round ${round}: page.close() ${outcome}`);
    if (outcome === 'NEVER SETTLED') hangs++;
  }
  console.log(`${hangs}/10 rounds hung`);
  process.exit(0);
})();

Expected behavior

page.close() always settles — resolves once the page is gone, or rejects with an error.

Actual behavior

On 7–8 of 10 rounds (Chromium only), the page.close() promise never settles — no resolution, no rejection, no timeout. .catch() never fires, so any loop awaiting close() freezes permanently and silently. In our accessibility-scanning harness this froze two long captures for 35+ minutes each on real W3C fixture pages before diagnosis.

Root cause (from a DEBUG=pw:protocol trace of a hanging round)

  • Playwright sends Target.closeTarget {targetId}.
  • The browser replies {"result":{"success":true}}.
  • The target does not actually close: the meta-refresh navigation was mid-commit, and the same session then emits the redirect destination's full lifecycle — Page.frameNavigated, DOMContentLoaded, load, paint events, networkIdle — all after the successful-looking closeTarget response.
  • Target.targetDestroyed is never emitted; page.close() waits for it indefinitely.

The browser-side half reproduces without Playwright: a raw-CDP client (plain WebSocket) gets closeTarget → success:true with no targetDestroyed ever emitted and the target still listed in Target.getTargets (~3/10 rounds, ~45–60 ms commit window). We are filing a Chromium bug for that half and will cross-link it here. Playwright's half is that page.close() trusts the acknowledgment with no deadline or retry.

Additional context — matrix (10 rounds per cell, same repro)

close() close({runBeforeUnload:true}) context.close()
Chromium 8/10 hang 0/10 0/10
Firefox 0/10 0/10 0/10
WebKit 0/10 0/10 0/10

After a hang, context.close() still settles and destroys the page (8/8 recoveries) — the context teardown path is unaffected. Verified workarounds: page.close({runBeforeUnload: true}), or racing close() against a deadline and calling context.close() on expiry.

Possibly related: #33806 (hangs while waiting for pending navigations) — different in that no dialog is involved and the acknowledged-but-not-performed Target.closeTarget is visible in the protocol trace.

Environment

  • Operating System: Linux (kernel 7.0.0-30-generic)
  • Browser: Chromium (bundled, HeadlessChrome/151.0.7922.34)
  • Node.js: 24.14.1

Activity

  1. wittjeff commented on Aug 22, 2026

    @wittjeff
    Author

    The Chromium half is already tracked — and root-caused at the C++ level with a pending code change — in https://issues.chromium.org/issues/536385539 (Target.closeTarget is lost when a navigation commit swaps the main RenderFrameHost right after ClosePage; the pending ClosePage callback dies with the old RFH and is never re-issued). Our meta-refresh(0) scenario above is an additional real-world trigger of the same race; corroborating evidence and workaround data posted there as comment 2.

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