skip to content

Request Control

Taking a request out of the browser's hands: which handler catches it, and whether it then gets a canned answer, a network error, a modified forward, or a reply from a recording.

on this pageshow

explore

questions

21

In Playwright, how does route.fulfill() answer a request without it reaching the network?

level: juniorimportance: must knowfreq 84%

answer

  1. The test answers, the server never hears
  2. One route, one settlement
  3. Object body versus file body
  4. Status defaults to success
  5. contentType overrides the inferred type

basics

~20 s

route.fulfill() completes a Playwright route by returning a response the test supplies, so the browser never contacts the real server. You set the status, headers and body, or pass a JSON object or a file path instead.

solid answer

~40 s

`route.fulfill()` is how a handler registered with `page.route()` answers an intercepted request itself. It takes `status` (default `200`), `headers`, and one body source: `body` for a string or `Buffer`, `json` for an object that Playwright serialises and labels `application/json`, or `path` for a file whose content type is inferred from its extension. `contentType` sets the header directly, and `response` replies with an `APIResponse` you already have. For a weather dashboard you route the forecast URL and fulfil it with a fixed payload, so every run renders the same temperatures with no third-party call, no rate limit and no network. A route may be settled exactly once — fulfil, abort, continue or fall back — and the call should be awaited.

code

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

test('dashboard renders the stubbed forecast', async ({ page }) => {
  await page.route('**/api/forecast?city=oslo', async route => {
    await route.fulfill({
      status: 200,
      headers: { 'cache-control': 'no-store' },
      json: { city: 'Oslo', tempC: 21, condition: 'clear' },
    });
  });

  await page.goto('/dashboard?city=oslo');
  await expect(page.getByTestId('temperature')).toHaveText('21°C');
});

go deeper

for a junior

Recall that fulfil answers the request from the test itself and that json is the shortcut for an API stub. Know that the status defaults to 200 and that you await the call.

for a middle

Explain the three body sources and how the content type is decided for each, and why a route can only be settled once. Be able to say what the browser sees for a fulfilled response.

for a senior

Show judgment about stub fidelity: a fulfilled payload that has drifted from the real contract makes a green suite meaningless, so say how your fixtures stay honest and how narrowly you scope the URL you answer.

for a principal

Own the trade between hermetic fixtures and real payloads across a suite, including where fixtures live, who refreshes them, and how the team notices when a stubbed contract no longer matches production.

## What fulfilling a route means A handler registered with `page.route()` receives a `Route` object standing for one request the browser is trying to make. Until the handler settles that route the request is frozen: nothing has gone to the server and the page is still waiting. `route.fulfill()` settles it by writing a response **from the test process** — Playwright hands the browser a status line, headers and a body that you supply, and the real server is never contacted. That is the whole value. A weather dashboard that normally calls a third-party forecast API renders from a payload the test owns, so the temperature on screen is the temperature the assertion expects, on every run, offline, with no rate limit and no overnight weather change breaking a green suite. ## The three body sources `route.fulfill()` takes exactly one body, expressed one of three ways. | option | you pass | what Playwright does | |---|---|---| | `body` | a string or a `Buffer` | sends those bytes verbatim | | `json` | any serialisable value | serialises it and sets `Content-Type: application/json` unless you set one | | `path` | a path to a file | reads the file and infers the content type from its extension | - `json` is the everyday choice for an API stub: `route.fulfill({ json: { tempC: 21 } })` needs no manual `JSON.stringify` and no manual content type. - `path` keeps a large payload out of the test file. A relative path is resolved against the current working directory, not against the test file, which is the usual reason a fixture is "not found". - `body` covers what the other two do not: an HTML document, a CSV export, a `Buffer` holding a PNG. ## Status, headers and content type - `status` defaults to `200`, so a plain `route.fulfill({ json })` is a success response. - `headers` is a flat record of string values merged into the response — `{ 'cache-control': 'no-store' }`. - `contentType` is a shorthand for the `Content-Type` header and wins over the type inferred from `path`. - `response` takes an `APIResponse` and replies with its status, headers and body, which is how you serve a real response you fetched first; the other options override individual fields of it. The status and header values themselves are ordinary HTTP semantics — Playwright neither validates nor interprets them, it just delivers what you asked for. ## A route is settled exactly once Think of the handler as owing the route one decision: 1. The browser starts a request and Playwright pauses it. 2. Your handler inspects it, typically through `route.request()`. 3. It settles the route once — `route.fulfill()`, `route.abort()`, `route.continue()` or `route.fallback()`. 4. Playwright releases the browser with that outcome and the page continues. Settle it twice and Playwright throws `Route is already handled!`. Settle it zero times — an early `return` in a branch, or a throw before the fulfil — and the request stays pending until the test times out. Always `await` the fulfil call: `page.route()` waits for the promise your handler returns, so an un-awaited fulfil can race the end of the test. ## Practical shape of a stub - Register the route before the navigation that triggers the request; a handler added afterwards never sees it. - Stub the narrowest URL you can, so an unrelated request is not silently answered with forecast JSON. - Keep the stub payload in the shape the real API returns; a fulfilled body that omits a field the component reads produces a passing test against a payload production never sends. - Fulfil is a *response*, not a proxy: the request never leaves the browser, so nothing you fulfil can be observed by the real service or by anything downstream of it.

  • What does route.fulfill({ path: 'fixtures/forecast.json' }) send, and how is the content type decided?
    Playwright reads the file and sends its bytes as the body, inferring the content type from the extension — `application/json` for `.json`. A relative path resolves against the current working directory, not the test file, which is the usual cause of a missing-fixture error. An explicit `contentType` or `Content-Type` header overrides the inferred value.
  • What happens if a handler calls route.fulfill() twice for the same request?
    The second call throws `Route is already handled!`. A route owes exactly one settlement — fulfil, abort, continue or fallback — and Playwright treats a second one as a bug rather than replacing the first response. The same error appears when a handler fulfils and then also continues.

saying these in an interview costs you the question

  • Thinks fulfill also forwards the request to the server
  • Passes a plain object to body instead of json
  • Calls fulfill and then continue in one handler
  • Assumes a stubbed body is parsed without any content type
  • Forgets to await route.fulfill inside an async handler
  • Resolves fixture paths against the test file, not the working directory
open as a page

In Playwright, what does browserContext.routeFromHAR() do to the requests a page makes?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It installs a route that answers matching requests from a previously recorded HAR archive instead of the real server, so the test replays saved traffic. Requests the archive does not contain are aborted by default.

open as a page

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

level: juniorimportance: must knowfreq 78%

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.

open as a page

In Playwright, when do you call route.fallback() instead of route.continue()?

level: middleimportance: must knowfreq 57%

basics

~20 s

Call route.fallback() when this handler declines the request and another registered handler should decide; call route.continue() when the request should go straight to the real network. Continue skips every remaining handler, fallback runs the next one.

open as a page

In Playwright, what happens to a request that routeFromHAR() finds no matching entry for?

level: middleimportance: must knowfreq 52%

basics

~20 s

By default it is aborted, so the application sees a failed request rather than a response. Passing notFound: 'fallback' instead lets the request continue to the next matching route handler or to the real network.

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

In Playwright, what happens to a page's WebSocket traffic once page.routeWebSocket() handles it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The route replaces the server. Playwright hands the handler a WebSocketRoute and, in Playwright 1.63, makes no connection to the real endpoint unless connectToServer() is called, so the test answers the page's messages with send() and closes with close().

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 observe the frames a page sends and receives over a WebSocket?

level: juniorimportance: should knowfreq 38%

basics

~10 s

Playwright fires page.on('websocket') with a WebSocket object for each socket the page opens. Listen to that object's framesent and framereceived events and read frame.payload. The listener only watches; it cannot change or block traffic.

open as a page

In Playwright, what does the error code passed to route.abort() change?

level: middleimportance: should knowfreq 44%

basics

~20 s

It picks which browser-level failure the request reports — the default is failed, with codes such as connectionrefused, namenotresolved and blockedbyclient available. Page JavaScript still sees only a generic network error, so the code affects fidelity and diagnosis, not app branching.

open as a page

In Playwright, what changes when you pass update: true to routeFromHAR()?

level: middleimportance: should knowfreq 44%

basics

~20 s

The call stops serving from the file. Requests go to the real network, their traffic is recorded, and the HAR is rewritten when the context closes, so the same test that replays a recording can also refresh it.

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

In Playwright, what does WebSocketRoute.connectToServer() change about a routed socket's message flow?

level: middleimportance: should knowfreq 40%

basics

~20 s

It opens the real connection the route suppressed and returns a second WebSocketRoute for the server side. Messages then flow through automatically in both directions until onMessage is called on a side, which makes that direction the test's responsibility.

open as a page

How do you make a Playwright route return the real forecast response with one field changed?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Call route.fetch() to perform the real request, parse the returned APIResponse, edit the parsed body, then settle the route with route.fulfill({ response, json }). Passing the response keeps the real status and headers while the json option replaces the payload.

open as a page

A Playwright route handler runs but the page hangs until the test times out — why?

level: seniorimportance: should knowfreq 31%

basics

~20 s

The handler almost certainly returned without settling the route. Every route owes exactly one call to fulfill, abort, continue or fallback; miss it on any branch, or throw before reaching it, and the request stays pending until an unrelated assertion times out.

open as a page

A Playwright HAR replay leaves the weather dashboard's forecast panel empty in CI though it passes locally — how do you diagnose it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Establish first whether the archive answered the request at all. Then compare the URL the app asks for against the recorded one, and check that the archive's response bodies actually travelled into CI rather than staying in uncommitted attachment files.

open as a page

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

level: seniorimportance: should knowfreq 45%

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.

open as a page

How would you scope a Playwright recordHar capture before those archives are committed to a repository?

level: principalimportance: should knowfreq 28%

basics

~20 s

Decide before recording what belongs in the file: narrow urlFilter to the endpoints under test, pick minimal mode and a zip path so archives stay small and self-contained, and treat everything captured as permanently readable repository data.

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

In Playwright, when should a suite fully mock a WebSocket instead of routing it to the real server?

level: principalimportance: should knowfreq 28%

basics

~20 s

Mock fully when determinism, offline runs and speed matter more than fidelity, which covers the bulk of UI cases. Connect to the real server when the handshake is complex or integration itself is the point.

open as a page

When should a Playwright suite fulfil from fixtures instead of fetching and mutating the real API?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Fulfil from fixtures for everything that runs on every commit, because those tests must be fast, hermetic and deterministic. Reserve fetching and mutating the real response for a thin, separately scheduled layer whose job is noticing that the upstream contract has drifted.

open as a page