Version
1.63.0 (Firefox 155.0, build 1543)
Steps to reproduce
Save this as repro.mjs in a folder with playwright installed. Run node repro.mjs coop 200, then node repro.mjs none 200 to compare.
import http from 'node:http';
import { firefox } from 'playwright';
const mode = process.argv[2] ?? 'coop';
const trials = Number(process.argv[3] ?? 200);
const html = `<!doctype html><title>t</title><h1>hello</h1>
<script type="module">history.replaceState({ key: 1 }, '', location.href);</script>`;
const server = http.createServer((req, res) => {
const headers = { 'content-type': 'text/html' };
if (mode === 'coop') headers['cross-origin-opener-policy'] = 'same-origin';
res.writeHead(req.url === '/' ? 200 : 404, headers);
res.end(req.url === '/' ? html : '');
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const url = `http://127.0.0.1:${server.address().port}/`;
const browser = await firefox.launch();
let stalls = 0;
let done = 0;
async function worker() {
while (done < trials) {
done++;
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(url, { timeout: 10_000 });
} catch (error) {
if (String(error).includes('Timeout')) stalls++;
else throw error;
}
await context.close();
}
}
await Promise.all([worker(), worker(), worker(), worker()]);
console.log(`mode=${mode} trials=${done} goto timeouts=${stalls}`);
await browser.close();
server.close();
Expected behavior
Every goto resolves once the page has loaded.
Actual behavior
With the COOP header some navigations never resolve and time out, even though the page loaded fine. On my machine that was 12 of 200. Without the header it was 0 of 200.
When it happens, every request for the page has already finished. A later page.waitForLoadState('load') returns right away and says load already fired. Starting a second goto on the same page gets it moving again.
Additional context
This looks like the same problem as #42183, which was closed without a fix. Here is what I found digging into it.
A new page starts on about:blank. Navigating it to a document with Cross-Origin-Opener-Policy: same-origin makes Firefox replace the browsing context. Playwright's Firefox profile sets fission.webContentIsolationStrategy to 0, so the new context stays in the same content process.
Juggler creates a new SimpleChannel for the new context. Its uid is 'process-' + Services.appinfo.processID (juggler/content/main.js), which is the same as the old channel's uid, and message ids start again at 1. The browser side only clears its cache of answered request ids when the uid changes. So if the old channel's last response has not been acked yet, the new channel's message with the same id gets answered from that cache and is never delivered.
In our runs the lost message is usually Page.navigationCommitted, so goto waits forever. If the number of earlier messages shifts by one, a different message gets lost instead (executionContextCreated), and page.evaluate hangs.
Setting browser.tabs.remote.useCrossOriginOpenerPolicy to false through firefoxUserPrefs makes it go away. In a longer run that was 0 hangs in 111, against 25 in 111 without it. Sending COEP as well also makes it go away, since the new context then lands in a new process.
A uid that is unique to each channel, for example one that includes the browsing context id, looks like it would fix it. The replacement would then go through the same handshake a cross-process swap already uses. I have not built or tested that change.
Environment
- OS: Windows 11
- Node.js: 24.14.1
- Playwright: 1.63.0, Firefox 155.0
Version
1.63.0 (Firefox 155.0, build 1543)
Steps to reproduce
Save this as repro.mjs in a folder with playwright installed. Run
node repro.mjs coop 200, thennode repro.mjs none 200to compare.Expected behavior
Every goto resolves once the page has loaded.
Actual behavior
With the COOP header some navigations never resolve and time out, even though the page loaded fine. On my machine that was 12 of 200. Without the header it was 0 of 200.
When it happens, every request for the page has already finished. A later
page.waitForLoadState('load')returns right away and says load already fired. Starting a second goto on the same page gets it moving again.Additional context
This looks like the same problem as #42183, which was closed without a fix. Here is what I found digging into it.
A new page starts on about:blank. Navigating it to a document with
Cross-Origin-Opener-Policy: same-originmakes Firefox replace the browsing context. Playwright's Firefox profile setsfission.webContentIsolationStrategyto 0, so the new context stays in the same content process.Juggler creates a new SimpleChannel for the new context. Its uid is
'process-' + Services.appinfo.processID(juggler/content/main.js), which is the same as the old channel's uid, and message ids start again at 1. The browser side only clears its cache of answered request ids when the uid changes. So if the old channel's last response has not been acked yet, the new channel's message with the same id gets answered from that cache and is never delivered.In our runs the lost message is usually Page.navigationCommitted, so goto waits forever. If the number of earlier messages shifts by one, a different message gets lost instead (executionContextCreated), and page.evaluate hangs.
Setting
browser.tabs.remote.useCrossOriginOpenerPolicyto false through firefoxUserPrefs makes it go away. In a longer run that was 0 hangs in 111, against 25 in 111 without it. Sending COEP as well also makes it go away, since the new context then lands in a new process.A uid that is unique to each channel, for example one that includes the browsing context id, looks like it would fix it. The replacement would then go through the same handshake a cross-process swap already uses. I have not built or tested that change.
Environment