skip to content

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