skip to content

Your Playwright test calls page.routeWebSocket() but the dashboard still reaches the live socket — why?

level: seniorimportance: should knowfreq 45%

answer

  1. Prove what the page really opened
  2. Registration order beats everything else
  3. Match the socket URL, not the page URL
  4. It may not be a socket at all
  5. One page, or the whole context

basics

~20 s

Usually the handler never ran: the route was registered after the navigation that opened the socket, the pattern does not match the ws:// URL, the traffic is not a WebSocket, or another page opened it.

solid answer

~40 s

Start by proving what the page actually opened: attach `page.on('websocket')` and log `ws.url()`. Then work through four causes. **Ordering** — only sockets created after `page.routeWebSocket()` are routed, so a route registered after `page.goto()` misses the connection entirely. **Matching** — the pattern runs against the `ws://` or `wss://` URL, which often lives on a different host from the page, and query strings or path prefixes break naive globs; a `RegExp` or a predicate over the `URL` is more forgiving. **Transport** — a client that fell back to HTTP long-polling or is using server-sent events never creates a WebSocket. **Scope** — a socket opened by a popup or another page is not covered by a route registered on one page; use `browserContext.routeWebSocket()` instead.

code

typescript · 17 lines
typescript
import { test, expect } from '@playwright/test';

test('route is registered before the socket is created', async ({ page }) => {
  page.on('websocket', ws => console.log('socket opened:', ws.url()));

  await page.routeWebSocket(
    url => url.pathname.includes('/subscribe'),
    ws => {
      console.log('route matched:', ws.url());
      ws.onMessage(() => ws.send(JSON.stringify({ city: 'Lisbon', temperature: 21 })));
    },
  );

  await page.goto('/dashboard');

  await expect(page.getByTestId('temperature')).toHaveText('21°C');
});

go deeper

for a junior

Check the boring things first: the route must be registered before the page opens the socket, and the pattern must match the ws or wss URL rather than the page address.

for a middle

Explain why the failure is silent — an unmatched route produces no error — and use page.on('websocket') to print the real URL before adjusting the pattern.

for a senior

Work a repeatable diagnosis: log socket URLs, log inside the handler to separate matching bugs from conversation bugs, widen the pattern to a true predicate, then narrow it again.

for a principal

Push the fix upstream. Make route registration part of a shared fixture so ordering cannot be got wrong per test, and decide how the suite detects a feed URL that changed.

## First, prove what the page opened Before theorising, get the facts. Attach the read-only listener and print the URL of every socket the page creates: ```ts page.on('websocket', ws => console.log('socket opened:', ws.url())); ``` If nothing prints, the page is not opening a WebSocket at all and no route will ever help. If something prints, you now have the exact string your pattern has to match — and on a weather dashboard it is frequently something like `wss://stream.forecast-vendor.example/v2/subscribe?city=lisbon&token=…`, nothing like the page's own origin. ## Cause 1: the route was registered too late `page.routeWebSocket()` only affects sockets created **after** it returns. A dashboard that opens its feed during initial hydration has already connected by the time a route registered after `page.goto()` exists. This is the single most common cause, and it is invisible: no error, no warning, just a handler that never runs. - Register the route before the navigation, and `await` the call. - In a fixture that navigates for you, register inside the fixture before it navigates, not in the test body afterwards. - If the app opens the socket on a later interaction, registering before that interaction is enough. ## Cause 2: the pattern does not match The matcher runs against the socket URL, not the page URL. Three things go wrong: 1. **Wrong host.** The feed streams from a vendor host while the pattern was built from the page's origin, so it matches nothing. 2. **Scheme.** The URL is `ws://` or `wss://`; a pattern written for `https://` will not match. 3. **Query strings and prefixes.** A glob that stops at the path misses a URL carrying `?token=…`. A `RegExp` on a distinctive path fragment, or a predicate over the `URL` object, is far more robust than a hand-tuned glob: ```ts await page.routeWebSocket(url => url.pathname.includes('/subscribe'), ws => { /* … */ }); ``` ## Cause 3: it is not a WebSocket Sockets are only one way to push. If the app uses server-sent events, or a client library that negotiated an HTTP long-polling transport, the traffic is ordinary HTTP and belongs to HTTP-level interception, not to socket routing. The `page.on('websocket')` probe settles this in one run: no event, no socket. ## Cause 4: the socket belongs elsewhere A route on one `page` covers that page. If the dashboard opens its feed from a popup, a second tab, or a page created later in the same context, register at context level with `browserContext.routeWebSocket()` so every page in the context is covered. ## A diagnostic order that saves time 1. Log every `ws.url()` with `page.on('websocket')`. 2. Add a `console.log` as the first line of the route handler — if it prints, the route matched and the bug is inside the handler. 3. If it does not print, temporarily widen the pattern to a predicate returning `true`, and see whether the handler runs at all. 4. Narrow the predicate back down once you know the real URL. ## When the handler does run but the test still fails A matched route with a broken conversation looks similar from the outside. The dashboard sits on its loading state because the app's subscribe frame was never answered; or the payload was sent as a plain object rather than a serialised string and the app's parser threw; or the frames were sent before the app finished registering its own message listener. Those are handler bugs, not matching bugs — and step 2 above is what tells the two apart. ## Making the failure loud The worst property of this bug is its silence: an unmatched route is indistinguishable from a route that matched and did nothing useful. Two cheap habits remove the ambiguity for good. - **Assert that interception happened.** Have the handler push `ws.url()` into an array the test owns, and assert that array is non-empty before asserting on the UI. A test that fails with "route never matched" is worth an hour of squinting at a stale tile. - **Register in the fixture that navigates.** When route setup lives in the same place that opens the page, no test can get the ordering wrong, and a changed feed URL becomes a one-line fix instead of a sweep through the suite.

  • The route handler runs, but the dashboard never leaves its loading state. Where do you look?
    Inside the handler. Without `connectToServer()`, anything the page sends goes nowhere unless you answer it, so an unacknowledged subscribe or auth frame leaves the app waiting. Log the payloads reaching `ws.onMessage()`, then `ws.send()` the acknowledgement the app expects before pushing data frames.
  • How would you route a socket the app only opens after a user action?
    Register before that action rather than before the navigation. Only sockets created after `page.routeWebSocket()` are routed, so as long as the call completes before the click that opens the channel, the handler will run. Registering at context level also works when the action opens the socket from a popup.

saying these in an interview costs you the question

  • Registers the route after the navigation and blames flakiness
  • Builds the pattern from the page origin instead of the socket URL
  • Writes an https pattern for a wss URL
  • Assumes every push channel is a WebSocket
  • Routes on one page when a popup opens the socket
  • Never logs ws.url() before guessing at patterns