skip to content

Fulfil and Abort

What a route handler may do with the request it caught: answer it from the test, fail it, forward a modified copy, or fetch the real response and mutate it before replying.

on this pageshow

explore

questions

6

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, 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 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

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

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