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

  1. Re-establish the connection: restart the relay and re-run `establishExtensionConnection` before sending more commands.
  2. Treat readyState!=OPEN as a signal to tear down and reconnect rather than retrying the same send.
  3. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/d78fe2d94e8928db. Report an issue: GitHub.