Version
1.61.1
Steps to reproduce
- Save the spec below as
repro.spec.ts (self-contained: it starts its own tiny
WebSocket server with node:http, no extra dependencies, no playwright.config needed).
npm init playwright@latest (defaults) or any project with @playwright/test@1.61.x.
npx playwright test repro.spec.ts --project=firefox
import { createHash } from 'node:crypto';
import http from 'node:http';
import { test } from '@playwright/test';
import type { AddressInfo } from 'node:net';
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// Minimal WebSocket server: completes the 101 upgrade handshake so the
// client-side `open` event fires. No frames are exchanged.
function createWsServer(): Promise<{ port: number; close: () => void }> {
const server = http.createServer((_, response) => {
response.setHeader('content-type', 'text/html');
response.end('<!DOCTYPE html><title>repro</title>');
});
server.on('upgrade', (request, socket) => {
const key = request.headers['sec-websocket-key'] ?? '';
const accept = createHash('sha1').update(`${key}${WS_GUID}`).digest('base64');
socket.write(
[
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${accept}`,
'',
'',
].join('\r\n'),
);
});
return new Promise(resolve => {
server.listen(0, '127.0.0.1', () => {
const { port } = server.address() as AddressInfo;
resolve({ port, close: () => server.close() });
});
});
}
test('WebSocket opened inside a dedicated worker crashes Firefox session', async ({ page }) => {
const { port, close } = await createWsServer();
try {
await page.goto(`http://127.0.0.1:${port}/`);
await page.evaluate(wsPort => {
const code = `
const ws = new WebSocket("ws://127.0.0.1:${wsPort}/");
ws.addEventListener("open", () => self.postMessage("ws-open"));
`;
const worker = new Worker(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })));
worker.addEventListener('message', event => console.log('worker message:', event.data));
}, port);
// Firefox: fails here with a bare "Error: Assertion error"
await page.waitForTimeout(5000);
} finally {
close();
}
});
test('control: WebSocket opened in the main page works fine', async ({ page }) => {
const { port, close } = await createWsServer();
try {
await page.goto(`http://127.0.0.1:${port}/`);
await page.evaluate(wsPort => {
const ws = new WebSocket(`ws://127.0.0.1:${wsPort}/`);
ws.addEventListener('open', () => console.log('main ws-open'));
}, port);
await page.waitForTimeout(5000);
} finally {
close();
}
});
Expected behavior
Both tests pass. A WebSocket opened from inside a dedicated Web Worker should not
affect the test. This is the case on chromium and webkit, and it was the case on
firefox up to Playwright 1.60.0.
Actual behavior
On firefox, the worker test dies with a bare protocol-level error as soon as the
WebSocket finishes opening inside the worker:
Error: Assertion error
Error: page.waitForTimeout: Test ended.
The control test (same WebSocket opened from the main page) passes.
Both classic workers and { type: "module" } workers reproduce it.
The worker's "open" event does fire (console shows "worker message: ws-open")
right before the session is torn down.
Additional context
Regression window (bisected):
| @playwright/test |
bundled Firefox |
result |
| 1.59.1 |
148.0.2 (r1511) |
✅ pass |
| 1.60.0 |
150.0.2 (r1522) |
✅ pass |
| 1.61.0 |
151.0 (r1532) |
❌ Assertion error |
| 1.61.1 |
151.0 (r1532) |
❌ Assertion error |
| 1.62.0-alpha-2026-07-10 |
152.0.4 (r1534) |
❌ Assertion error |
Protocol trace right before the teardown (DEBUG=pw:protocol):
◀ RECV {"method":"Page.webSocketOpened","params":{"frameId":"mainframe-10","requestId":"12","wsid":"1","effectiveURL":"ws://127.0.0.1:52914/"},...}
◀ RECV {"method":"Runtime.console","params":{"args":[{"value":"worker message:"},{"value":"ws-open"}],...}}
◀ RECV {"method":"Browser.detachedFromTarget","params":{...}}
Where the assert fires — ffPage.ts _onWebSocketOpened:
_onWebSocketOpened(event) {
const request = this._webSocketRequests.get(event.requestId);
assert(request); // <-- throws: the worker's WS upgrade request was never
// registered in _webSocketRequests
...
}
When the WebSocket originates from a dedicated worker, its upgrade request never
went through the code path that populates _webSocketRequests, so the assert
throws and the whole page session is torn down.
There is also this in the firefox process stderr around the same moment
(DEBUG=pw:browser), possibly secondary:
JavaScript error: chrome://juggler/content/Helper.js, line 82:
NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE)
[nsIWebProgress.removeProgressListener]
Real-world impact: any app tested against a Vite dev server where a Web Worker
loads Vite's HMR client is affected — in dev mode Vite injects
import ... from "/@vite/client" into worker scripts it serves, the client opens
its HMR WebSocket inside the worker, and every firefox test touching such a page
dies instantly. We hit this via the pdfjs-dist worker in a Vue 3 + Vite 8 app;
grep-ability keywords: pdf.js, "Setting up fake worker", vite HMR.
Environment
System:
OS: macOS 26.5.1
CPU: (8) arm64 Apple M2
Binaries:
Node: 24.18.0
npm: 11.16.0
pnpm: 11.11.0
IDEs:
Languages:
Bash: 3.2.57
npmPackages:
@playwright/test: ^1.61.1 => 1.61.1
Version
1.61.1
Steps to reproduce
repro.spec.ts(self-contained: it starts its own tinyWebSocket server with
node:http, no extra dependencies, no playwright.config needed).npm init playwright@latest(defaults) or any project with@playwright/test@1.61.x.npx playwright test repro.spec.ts --project=firefoxExpected behavior
Both tests pass. A WebSocket opened from inside a dedicated Web Worker should not
affect the test. This is the case on chromium and webkit, and it was the case on
firefox up to Playwright 1.60.0.
Actual behavior
On firefox, the worker test dies with a bare protocol-level error as soon as the
WebSocket finishes opening inside the worker:
Error: Assertion error
Error: page.waitForTimeout: Test ended.
The control test (same WebSocket opened from the main page) passes.
Both classic workers and { type: "module" } workers reproduce it.
The worker's "open" event does fire (console shows "worker message: ws-open")
right before the session is torn down.
Additional context
Regression window (bisected):
Protocol trace right before the teardown (
DEBUG=pw:protocol):Where the assert fires —
ffPage.ts_onWebSocketOpened:When the WebSocket originates from a dedicated worker, its upgrade request never
went through the code path that populates
_webSocketRequests, so the assertthrows and the whole page session is torn down.
There is also this in the firefox process stderr around the same moment
(
DEBUG=pw:browser), possibly secondary:Real-world impact: any app tested against a Vite dev server where a Web Worker
loads Vite's HMR client is affected — in dev mode Vite injects
import ... from "/@vite/client"into worker scripts it serves, the client opensits HMR WebSocket inside the worker, and every firefox test touching such a page
dies instantly. We hit this via the pdfjs-dist worker in a Vue 3 + Vite 8 app;
grep-ability keywords: pdf.js, "Setting up fake worker", vite HMR.Environment
System: OS: macOS 26.5.1 CPU: (8) arm64 Apple M2 Binaries: Node: 24.18.0 npm: 11.16.0 pnpm: 11.11.0 IDEs: Languages: Bash: 3.2.57 npmPackages: @playwright/test: ^1.61.1 => 1.61.1