Repository navigation
[Bug]: Firefox BiDi: page.title() intermittently returns an empty string after navigation #43644
Description
Version
1.62.0
Steps to reproduce
-
Run Firefox 158.0.1 with Playwright's WebDriver BiDi backend on Windows Server 2022.
-
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:blanktab runs in Firefox'sprivilegedaboutprocess, so the first navigation switches processes. -
Apply CPU load using three busy threads.
-
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:
- Playwright starts initializing its UtilityScript while its frame still references the previous utility execution context (
c478be8c). - Firefox creates the new utility sandbox (
e812942f). - Playwright registers the new utility context and destroys the old execution context before the pending initialization operation completes.
- Firefox returns a successful BiDi response for request
id=13, associated with the new sandbox. - 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.
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]()playwright/packages/playwright-core/src/server/bidi/bidiPage.ts
Lines 135 to 201 in a2b65a0
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
Miguel0888 commentedon Oct 26, 2026
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.
sunny-kavixo commentedon Oct 26, 2026
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:
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))
dgozman commentedon Oct 26, 2026
cc Henrik Skupin (@whimboo)
whimboo commentedon Oct 26, 2026
Holger Benl (@hbenl) could you please take a look?