microsoft/playwright · error · Error
Unexpected WebSocket state: ${this._ws.readyState}
Error message
Unexpected WebSocket state: ${this._ws.readyState} What it means
Thrown by the relay's `ExtensionConnection.send` when the underlying WebSocket's `readyState` is not `OPEN`. The guard prevents queueing a send on a closing/closed socket. The numeric readyState (e.g. 2=CLOSING, 3=CLOSED) is included for diagnosis.
Source
Thrown at packages/playwright-core/src/tools/mcp/cdpRelay.ts:309
class ExtensionConnection {
private readonly _ws: WebSocket;
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: Error) => void, error: Error }>();
private _lastId = 0;
onmessage?: <M extends keyof ExtensionEventsV2>(method: M, params: ExtensionEventsV2[M]['params']) => void;
onclose?: (reason: string) => void;
constructor(ws: WebSocket) {
this._ws = ws;
this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this));
this._ws.on('error', this._onError.bind(this));
}
async send<M extends keyof ExtensionCommandV2>(method: M, params: ExtensionCommandV2[M]['params']): Promise<any> {
if (this._ws.readyState !== ws.OPEN)
throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`);
const id = ++this._lastId;
this._ws.send(JSON.stringify({ id, method, params }));
const error = new Error(`Protocol error: ${method}`);
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error });
});
}
close(message: string) {
debugLogger('closing extension connection:', message);
if (this._ws.readyState === ws.OPEN)
this._ws.close(1000, message);
}
private _onMessage(event: websocket.RawData) {
const eventData = event.toString();
let parsedJson;
try {View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Re-establish the connection: restart the relay and re-run `establishExtensionConnection` before sending more commands.
- Treat readyState!=OPEN as a signal to tear down and reconnect rather than retrying the same send.
- Ensure no commands are queued after intentionally calling `close()` on the connection.
Defensive patterns
Strategy: validation
Validate before calling
// Guard sends against non-OPEN sockets.
import ws from 'ws';
function assertOpen(socket: ws) {
if (socket.readyState !== ws.OPEN)
throw new Error(`WebSocket not open (state=${socket.readyState}); reconnect first.`);
} Try / catch
try {
await conn.send(method, params);
} catch (e) {
if (/Unexpected WebSocket state/.test((e as Error).message)) {
await relay.establishExtensionConnection(clientName); // reconnect
await conn.send(method, params);
} else throw e;
} Prevention
- Check readyState===OPEN before sending, or rely on a connection-ready gate.
- Treat any non-OPEN state as 'reconnect required', not 'retry send'.
- Drain queued commands on close rather than firing them into a dead socket.
When it happens
Trigger: A command is dispatched to the extension after the WebSocket has started closing or fully closed — e.g. the extension disconnected, the relay called `close()`, or a network error transitioned the socket out of OPEN. Any in-flight or subsequent `send` then hits this guard.
Common situations: The user closed the browser/extension mid-session; an idle timeout or network drop closed the WebSocket; a protocol error triggered `close()` and a queued command fires afterward; the extension was reloaded.
Related errors
- Extension not connected
- Failed to create tab
- Unsupported channel: "${this._browserChannel}"
- "${this._browserChannel}" executable not found. Make sure it
- Malformed endpoint. Did you use BrowserType.launchServer met
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/d78fe2d94e8928db.
Report an issue: GitHub.