skip to content

Matching a Route

Registering a handler with page.route or context.route and matching by glob, regex or predicate. Interviewers probe it because precedence and timing decide whether a stub fires at all.

on this pageshow

explore

questions

5

In Playwright, what URL matchers can you pass to page.route() when intercepting a request?

level: juniorimportance: must knowfreq 78%

answer

  1. Three shapes, not just a string
  2. Wildcards behave like path globs
  3. A regex here is not anchored
  4. A callback receives a parsed URL
  5. Matching never sees the HTTP method

basics

~20 s

Playwright's page.route() accepts three matcher forms: a glob string, a RegExp tested against the whole URL, or a predicate function receiving a parsed URL object. Only the URL is matched; method and headers are checked inside the handler.

solid answer

~40 s

`page.route(matcher, handler)` parks every request whose URL matches and hands it to your handler. The matcher takes three forms. A **glob string** like `'**/v1/forecast*'`, where `*` stops at a `/` and `**` crosses one, which is why patterns usually start with `**/`. A `RegExp` like `/\/v1\/forecast/`, tested against the full URL and unanchored, so it matches anywhere in the string. A **predicate** `(url: URL) => boolean`, which gets a parsed `URL` and can compare `url.pathname` or read `url.searchParams`. A relative string such as `'/v1/forecast'` is merged with the context's `baseURL`. Matching only ever sees the URL, so to stub just POSTs you match the endpoint broadly and branch on `route.request().method()` inside the handler. `context.route()` accepts the same forms and covers every page in the context.

code

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

test('forecast panel renders stubbed data', async ({ page }) => {
  // Glob: any host, any query string.
  await page.route('**/v1/forecast*', route =>
    route.fulfill({ json: { tempC: 21 } }));

  // Predicate: exact path plus a query-parameter check.
  await page.route(
    url => url.pathname === '/v1/alerts' && url.searchParams.get('city') === 'oslo',
    route => route.fulfill({ json: [] }),
  );

  await page.goto('/dashboard');
  await expect(page.getByTestId('temp')).toHaveText('21');
});

go deeper

for a junior

Remember the three matcher forms, and that a route must be registered before the navigation that triggers the request. Start with a glob and reach for a regex only when a glob cannot express the shape.

for a middle

Explain why a RegExp matcher is a substring test rather than an exact one, how a relative pattern is merged with baseURL, and why a glob normally starts with two asterisks.

for a senior

Know that matching is URL-only, so method or body filtering happens inside the handler, and be ready to say why an over-broad matcher swallows unrelated traffic and hides real regressions.

for a principal

Set the house convention: which matcher form the suite standardises on, where endpoint patterns are declared and shared, and how you stop broad globs from silently faking traffic no test intended to fake.

## What a route registration does `page.route(matcher, handler)` tells Playwright that, on this page, any request whose URL satisfies `matcher` should be handed to `handler` instead of going straight to the network. Every request the page makes is tested: the document load, `fetch` and XHR calls, images, stylesheets, and requests from its iframes. On a match the request is **paused** and the handler is called with a `Route` object and the `Request`. Nothing reaches the network until the handler resolves the route; if it forgets to, the request hangs and the action eventually times out, which shows up as a mysteriously slow test rather than a routing error. `context.route()` is the same registration one level up. It applies to every page already open in the browser context and to every page opened later, which is the only way to cover a popup the app opens for itself. Registration is **not retroactive**. A handler installed after `page.goto()` cannot catch the requests that navigation already started, which is the single most common reason a stub "does not work". ## The three matcher forms 1. **Glob string** — `page.route('**/v1/forecast*', handler)`. `*` matches a run of characters that does not cross a `/`; `**` crosses `/` freely. Because a full URL carries a scheme and a host, a pattern meaning "this path on any host" begins with `**/`, and one that must survive a cache-busting query string ends with `*`. 2. **RegExp** — `page.route(/\/v1\/forecast/, handler)`. The expression is tested against the whole URL string and is **not anchored**, so it behaves exactly like `regexp.test(url)`: `/forecast/` also matches `https://api.weather.example/v1/forecast-archive` and any URL with `forecast` in a query value. 3. **Predicate** — `page.route(url => url.pathname === '/v1/forecast', handler)`. The callback receives a parsed `URL`, so you can compare `url.pathname` exactly, read `url.searchParams.get('city')`, or check `url.host` in ordinary JavaScript instead of pattern punctuation. | Matcher | Best at | Watch out for | |---|---|---| | Glob string | readable "this endpoint, any host" patterns | `*` will not cross a `/`; a query string needs a trailing `*` | | `RegExp` | shapes a glob cannot express, such as digits in a path | unanchored substring matching also catches near neighbours | | Predicate | exact paths and query-parameter logic | opaque in review; there is no pattern left to grep for | For anything more elaborate than `*` and `**`, reach for a `RegExp` rather than guessing at extra glob punctuation. ## baseURL and relative matchers When the browser context has a `baseURL` — usually set through `use: { baseURL }` in the Playwright config — a matcher string that is a bare path is merged with it using the `URL` constructor. So `page.route('/v1/forecast', handler)` matches that path **on the base host only**. That is convenient when you stub your own API and a trap when the request you care about goes somewhere else: a weather dashboard calling a third-party forecast host will not be matched by a relative pattern. Keep such patterns absolute, or start them with `**/`. ## What the matcher cannot see Matching is decided on the URL and nothing else: - **Method, headers and post body are invisible to the matcher.** Match the endpoint broadly, then branch inside the handler on `route.request().method()` and hand the rest on with `route.fallback()`. - **The response is invisible.** A route decides a request's fate before it is sent, so you cannot match on a status code. - **Service Worker traffic is not intercepted by default.** If the dashboard registers a Service Worker that answers `/v1/forecast` from a cache, the route never fires; the context option `serviceWorkers: 'block'` makes interception deterministic. - **A page-scoped route does not cover a popup.** A window the app opens is a separate `Page`, so it needs `context.route()`. ## Choosing a form - Reach for a **glob** by default. It reads well in review and is the shortest way to say "this endpoint, whatever the host and query string". - Reach for a **RegExp** when part of the path varies in a way a glob cannot narrow, such as `/\/v1\/stations\/\d+$/` — and anchor it, because it is otherwise a substring test. - Reach for a **predicate** when the real decision is about query parameters, or when the condition is simply easier to read as code. - Prefer the **narrowest pattern that still matches**. A stray `'**/*'` parks every image and stylesheet on the page as well, and a handler that answers all of them slows the test to a crawl. ## A worked set For a weather dashboard backed by a third-party forecast API, three registrations cover the usual needs: `'**/v1/forecast*'` for the main endpoint on any host, `/\/v1\/stations\/\d+$/` for a numeric station lookup, and a predicate for `/v1/alerts` restricted to `city=oslo`. Install all three before the `page.goto()` that triggers the load, and remember that whichever form you pick, only the URL is being compared.

  • Why does a RegExp route matcher in Playwright sometimes intercept more requests than you expected?
    The expression is tested against the whole URL and is not anchored, so it behaves like `regexp.test(url)`. A pattern of `/forecast/` also matches `/v1/forecast-archive` and any URL carrying `forecast` in a query value. Anchor it, or switch to a predicate that compares `url.pathname` exactly.
  • How would you route only the POST requests to an endpoint you are intercepting with Playwright?
    The matcher only ever sees the URL, so match the endpoint broadly and branch inside the handler on `route.request().method()`. Requests you did not mean to stub are passed on with `route.fallback()`, which hands them to the next matching handler and to the network if none is left.

saying these in an interview costs you the question

  • Thinks a route registered after page.goto still catches the load request.
  • Assumes a RegExp matcher is anchored to the exact path.
  • Believes the matcher can filter on HTTP method or headers.
  • Writes a single-asterisk pattern and expects it to cross slashes.
  • Thinks page.route also covers popups opened by the page.
open as a page

In Playwright, if a page.route() and a context.route() handler both match a request, which one runs first?

level: middleimportance: must knowfreq 58%

basics

~10 s

Playwright consults page-level handlers before context-level ones, and within each scope the most recently registered matching handler wins. Earlier handlers run only if the winner steps aside with route.fallback().

open as a page

A Playwright test stubs the forecast endpoint with page.route(), but the real API is still called. How do you find out why the handler never fired?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Log the real URLs with a catch-all page.route that falls back, then compare them to your matcher. Usual causes: the route was registered after page.goto, the pattern misses the host or query string, or a Service Worker answers the request.

open as a page

In Playwright, how do you stop a page.route() handler from applying for the rest of a test?

level: middleimportance: should knowfreq 42%

basics

~10 s

Playwright removes handlers with page.unroute(matcher, handler) for one registration or page.unrouteAll() for all of them, and passing a times option to page.route() retires a handler automatically after that many matches.

open as a page

How would you structure shared Playwright route stubs so individual tests can still override one endpoint?

level: principalimportance: should knowfreq 33%

basics

~20 s

Register broad defaults with context.route in an auto fixture, and let a test override one endpoint with page.route, which is consulted first. Keep the URL patterns in one shared module so a default and its override cannot drift apart.

open as a page