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
- Route WebSockets from exactly one client per browser context.
- Give the second client its own browser context (newContext) so it can install its own binding.
- 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
- Route WebSockets from exactly one connection per browser context.
- When sharing a browser across MCP clients, assign each its own context.
- Disconnect/unroute the prior client before a new one routes WebSockets.
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
- Route from har is not supported in thin clients
- Route is already handled!
- No tab found for sessionId: ${sessionId}
- Extension not connected
- Unexpected WebSocket state: ${this._ws.readyState}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/616632ef16c72272.
Report an issue: GitHub.