microsoft/playwright · error · Error

Another client is already routing WebSockets

Error message

Another client is already routing WebSockets

What it means

WebSocket interception works by installing a single named binding (kBindingName) on a browser context. WebSocketRouteDispatcher.install records which connection registered the binding; if a different connection then tries to install it, the guard throws. Only one client may route WebSockets per context at a time.

Source

Thrown at packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts:66

          if (frame === this._frame)
            this._executionContextGone();
        }),
        eventsHelper.addEventListener(frame._page, Page.Events.FrameDetached, (frame: Frame) => {
          if (frame === this._frame)
            this._executionContextGone();
        }),
        eventsHelper.addEventListener(frame._page, Page.Events.Close, () => this._executionContextGone()),
        eventsHelper.addEventListener(frame._page, Page.Events.Crash, () => this._executionContextGone()),
    );
    WebSocketRouteDispatcher._idToDispatcher.set(this._id, this);
    (scope as any)._dispatchEvent('webSocketRoute', { webSocketRoute: this });
  }

  static async install(progress: Progress, connection: DispatcherConnection, target: Page | BrowserContext): Promise<InitScript> {
    const context = target instanceof Page ? target.browserContext : target;
    let data = context.getBindingClient(kBindingName) as BindingData | undefined;
    if (data && data.connection !== connection)
      throw new Error('Another client is already routing WebSockets');
    if (!data) {
      data = { counter: 0, connection, binding: null as any };
      data.binding = await context.exposeBinding(progress, kBindingName, (source, payload: ws.BindingPayload) => {
        if (payload.type === 'onCreate') {
          const contextDispatcher = connection.existingDispatcher<BrowserContextDispatcher>(context);
          const pageDispatcher = contextDispatcher ? PageDispatcher.fromNullable(contextDispatcher, source.page) : undefined;
          let scope: PageDispatcher | BrowserContextDispatcher | undefined;
          if (pageDispatcher && matchesPattern(pageDispatcher, context._options.baseURL, payload.url))
            scope = pageDispatcher;
          else if (contextDispatcher && matchesPattern(contextDispatcher, context._options.baseURL, payload.url))
            scope = contextDispatcher;
          if (scope) {
            new WebSocketRouteDispatcher(scope, payload.id, payload.url, payload.protocols, source.frame);
          } else {
            const request: ws.PassthroughRequest = { id: payload.id, type: 'passthrough' };
            source.frame.evaluateExpression(progress, `globalThis.__pwWebSocketDispatch(${JSON.stringify(request)})`).catch(() => {});
          }
          return;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Route WebSockets from exactly one client per browser context.
  2. Give the second client its own browser context (newContext) so it can install its own binding.
  3. Ensure the previous connection fully disconnects (closes the context or unroutes) before a new client routes WebSockets.

Example fix

// before — two clients, same context
await ctx1.routeWebSocket('**/ws', handler1);
await ctx2.routeWebSocket('**/ws', handler2); // throws if ctx1===ctx2
// after — separate contexts
const ctx2 = await browser.newContext();
await ctx2.routeWebSocket('**/ws', handler2);
Defensive patterns

Strategy: validation

Validate before calling

// Route WebSockets from a single client per context.
// Before a second client routes, give it its own context.
const ctx2 = await browser.newContext();
await ctx2.routeWebSocket('**/ws', handler);

Prevention

When it happens

Trigger: Two separate Playwright connections (e.g. two MCP clients, or a client plus a second test worker) both call context.routeWebSocket() on the same shared browser context; reconnecting a new client without the previous one having removed the binding.

Common situations: Multiple MCP sessions attached to one shared browser context for collaborative automation; a long-lived context reused across reconnects where the prior session's routeWebSocket binding persists.

Related errors


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