Skip to content

[Bug]: Firefox BiDi: page.title() intermittently returns an empty string after navigation #43644

Open

Description

@Miguel0888

Version

1.62.0

Steps to reproduce

  1. Run Firefox 158.0.1 with Playwright's WebDriver BiDi backend on Windows Server 2022.

  2. Launch a persistent context with a user data directory, close it, and launch it again using the same directory. On the second launch, the initial about:blank tab runs in Firefox's privilegedabout process, so the first navigation switches processes.

  3. Apply CPU load using three busy threads.

  4. Navigate to a page with a title and immediately retrieve the title:

    await page.goto('data:text/html,<title>persistent</title>');
    const title = await page.title();

The failure was observed in this scenario through Playwright4J. Independent reproduction with unmodified Node.js Playwright has not yet been confirmed.

Expected behavior

After page.goto() resolves at the load event, the first call to page.title() should return "persistent".

Actual behavior

Intermittently, the first call to page.title() returns an empty string ("") without throwing an exception. A second immediate call returns "persistent".

Observed failure rate in CI:

  • 7 out of 8 runs under CPU load.
  • 0 out of 8 runs without CPU load.

The attached WebDriver BiDi protocol log shows the following sequence:

  1. Playwright starts initializing its UtilityScript while its frame still references the previous utility execution context (c478be8c).
  2. Firefox creates the new utility sandbox (e812942f).
  3. Playwright registers the new utility context and destroys the old execution context before the pending initialization operation completes.
  4. Firefox returns a successful BiDi response for request id=13, associated with the new sandbox.
  5. Playwright has already rejected the pending operation locally because the old execution context was destroyed. The first title lookup therefore returns an empty string.

A subsequent title lookup uses the new utility context and returns "persistent".

The protocol response itself succeeds. The failure occurs in Playwright's handling of the execution-context lifecycle and the pending operation.

bidi-protocol-red-run.log

Additional context

This issue was observed through [Playwright4J](https://github.com/aresstack/playwright4j), which runs Playwright's 1.62.0 server bundle on the JVM using GraalJS and a Java client.

The issue has not yet been independently reproduced using unmodified Node.js Playwright. The analysis is based on the captured BiDi protocol traffic and the corresponding Playwright source code.

The event ordering observed in Firefox is not considered a confirmed Firefox bug. The suspected issue is in Playwright's BiDi backend: the frame retains a stale utility execution context after the new main realm has been observed.

Relevant source locations in Playwright:

  • [[BidiPage._onRealmCreated()](https://github.com/microsoft/playwright/blob/a2b65a057a78204d8411b068e1e28d0db23db44b/packages/playwright-core/src/server/bidi/bidiPage.ts#L135-L201) and realm lifecycle handling](
    private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
    if (realmInfo.type === 'dedicated-worker') {
    const delegate = new BidiExecutionContext(this._session, realmInfo);
    const worker = new Worker(this._page, realmInfo.origin);
    this._realmToWorkerContext.set(realmInfo.realm, worker.createExecutionContext(delegate));
    worker.workerScriptLoaded();
    this._page.addWorker(realmInfo.realm, worker);
    return;
    }
    if (this._contextIdToContext.has(realmInfo.realm))
    return;
    if (realmInfo.type !== 'window' || realmInfo.sandbox)
    return;
    const frame = this._page.frameManager.frame(realmInfo.context);
    if (!frame)
    return;
    const delegate = new BidiExecutionContext(this._session, realmInfo);
    const context = new dom.FrameExecutionContext(delegate, frame, 'main');
    frame.contextCreated('main', context);
    this._contextIdToContext.set(realmInfo.realm, context);
    this._createUtilityWorld(realmInfo, frame, context);
    }
    private async _createUtilityWorld(realmInfo: bidi.Script.WindowRealmInfo, frame: frames.Frame, mainContext: dom.FrameExecutionContext) {
    try {
    const result = await this._session.send('script.evaluate', {
    expression: '1 + 1',
    target: {
    context: realmInfo.context,
    sandbox: UTILITY_WORLD_NAME,
    },
    serializationOptions: {
    maxObjectDepth: 10,
    maxDomDepth: 10,
    },
    awaitPromise: true,
    userActivation: true,
    });
    if (await frame.mainContext() !== mainContext)
    return;
    const delegate = new BidiExecutionContext(this._session, {
    ...realmInfo,
    realm: result.realm,
    sandbox: UTILITY_WORLD_NAME
    });
    const utilityContext = new dom.FrameExecutionContext(delegate, frame, 'utility');
    frame.contextCreated('utility', utilityContext);
    this._contextIdToContext.set(result.realm, utilityContext);
    } catch (error) {
    debugLogger.log('error', error);
    }
    }
    _onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
    const context = this._contextIdToContext.get(params.realm);
    if (context) {
    this._contextIdToContext.delete(params.realm);
    context.frame.contextDestroyed(context);
    return true;
    }
    const existed = this._realmToWorkerContext.delete(params.realm);
    if (existed) {
    this._page.removeWorker(params.realm);
    return true;
    }
    return false;
    }
    )
  • [Frame._title()](https://github.com/microsoft/playwright/blob/a2b65a057a78204d8411b068e1e28d0db23db44b/packages/playwright-core/src/server/frames.ts#L1717-L1729)
  • [Playwright4J regression test](https://github.com/aresstack/playwright4j/blob/203c5debf78e5d2648036574568e69c65f87c11f/playwright4j/src/test/java/com/aresstack/playwright4j/graal/NodeCompatFirefoxUtilityWorldTest.java#L58-L68)

A client-side workaround has been implemented in Playwright4J to invalidate the previous utility context when a new main realm is observed. The regression test fails without this change and passes with it. The repeated Windows CI test improved from 7/8 failures under load to 8/8 successful runs after the fix.

The regression test is a JUnit test using GraalJS and mocked BiDi components. It is not directly runnable in the Playwright repository. A native Playwright regression test could be added if needed.

Environment

OS: Windows Server 2022 (GitHub Actions windows-2022)
CPU: 4 vCPU
Browser: Firefox 158.0.1
Protocol: WebDriver BiDi
Playwright server bundle: 1.62.0
Runtime: Playwright4J, JVM with GraalJS
Client: Java
Scenario: Persistent context reusing the same user data directory, process-switching navigation, and three busy CPU threads

Note: The failure has not yet been independently reproduced with unmodified Node.js Playwright. No corresponding Node.js Playwright environment report is available for this run.

Activity

  1. Miguel0888 commented on Oct 26, 2026

    @Miguel0888
    Author

    Follow-up: Root cause confirmed and Playwright4J workaround verified

    The investigation on the Playwright4J side is now complete.

    The observed Firefox BiDi event ordering is valid: destruction of the previous document's realm may be reported after the new document has finished loading. The failure is in Playwright's BiDi client-side lifecycle handling, which can retain a stale Utility World and reject a pending evaluation locally even though the corresponding BiDi response succeeds. This causes the first page.title() call to return an empty string.

    [Playwright4J PR #67](aresstack/playwright4j#67) implements a targeted workaround in the shim: when a new main realm is observed, the previous Utility World is invalidated immediately, avoiding the stale execution context.

    The targeted regression test reproduced the failure in 7 out of 8 runs under CPU load before the patch and passed all 8 runs afterward. The fix is included in Playwright4J 0.3.0 (commit 298f809).

    This is a client-side workaround, not an upstream fix in Playwright. It will remain in place until the underlying BiDi lifecycle handling is corrected upstream.

    Testing note: The regression test uses GraalJS and mocked BiDi components. An independent reproduction with unmodified Node.js Playwright has not yet been confirmed.

  2. sunny-kavixo commented on Oct 26, 2026

    @sunny-kavixo

    Hi Miguel0888 and Playwright maintainers,

    I'd like to help investigate the Firefox WebDriver BiDi execution-context lifecycle issue reported here.

    I understand that the Playwright4J workaround has already been implemented and verified, but independent reproduction with unmodified Node.js Playwright remains unconfirmed.

    My proposed approach is to:

    1. Review the BiDi realm-creation and destruction lifecycle, including the relevant Utility World handling.
    2. Attempt an independent reproduction using unmodified upstream Playwright and the reported Firefox BiDi scenario.
    3. Compare the reported version with current upstream main.
    4. If reproduced, add a targeted regression test covering navigation, delayed realm destruction, and stale utility-context invalidation.
    5. Prepare the smallest upstream fix and run focused validation without duplicating the Playwright4J workaround unnecessarily.

    I'll keep the existing Playwright4J work properly attributed and avoid claiming an upstream defect or fix without independent evidence.

    Would you be comfortable with me investigating this and submitting a focused PR if the upstream issue is confirmed?

    Thanks,
    Putluru Sandeep (Putluru Sandeep (@sunny-kavixo))

  3. dgozman commented on Oct 26, 2026

    @dgozman
    Collaborator
  4. whimboo commented on Oct 26, 2026

    @whimboo
    Collaborator

    Holger Benl (@hbenl) could you please take a look?

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

    bidiProblem around bidi

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions