Network Interception & Mocking
page.route intercepts requests and fulfills them with stubbed responses or aborts them. Network mocking removes third-party flakiness, simulates slow or failing APIs, and lets tests control exactly what the server returns.
What is this?
page.route(pattern, handler) intercepts matching requests in the browser. The handler can continue the request, fulfill it with a fabricated response, or abort it — all without touching the real server. This is how Playwright tests cover error states, slow networks and third-party APIs deterministically.
Why do you need it?
- Third-party APIs, ad networks and analytics are sources of flaky, non-reproducible test runs — mocking them makes tests deterministic.
- Error and edge states (500s, timeouts, empty lists) are nearly impossible to trigger reliably against a healthy backend.
- Slow-network and offline scenarios are testable by throttling or aborting requests instead of configuring infrastructure.
- Interception works at the browser layer, so it covers XHR, fetch, image loads and navigations uniformly.
- Combined with waitForResponse, you can assert that the app made the exact request you expected.
Mental model
A flight simulator pretending to be air traffic control
page.route intercepting an API call
Pilots (the app) still practice takeoffs, turbulence and engine failure without a real airport. The simulator (route handler) decides what the ground answers and even whether the runway is closed.
How it works
- Call page.route(pattern, handler) where pattern is a glob, regex or predicate matching request URLs.
- Every matching request is intercepted before it leaves the browser and routed to your handler.
- The handler calls route.continue() to pass the request through, route.fulfill() to answer with a stub, or route.abort() to fail it.
- Multiple routes are matched from last-registered to first, and a handler can choose to chain on.
- Calls to route.continue() are unthrottled by default, so intercepted requests no longer hit the network stack normally.
A request's journey when a route handler is registered.
Key commands
await page.route('**/api/orders', handler)Register a handler for requests whose URL matches the glob or regex.
route.fulfill({ status: 200, body: '{}' })Respond with a fabricated body, status and content type.
route.abort()Cancel the request as if it failed at the network layer.
await page.waitForResponse('**/api/orders')Resolve when a matching response arrives, then assert on its status.
route.continue({ headers: { Authorization: '...' } })Pass the request through while overriding headers, useful for injecting auth.
await context.route('**/ads/*', handler)Intercept on the context so every page and popup shares the handler.
Examples
Mocking an orders API and simulating a 500
import { test, expect } from "@playwright/test"; test("shows orders from a stubbed API", async ({ page }) => { await page.route("**/api/orders", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: 1, status: "shipped" }, { id: 2, status: "pending" }, ]), }) ); await page.goto("https://example.com/orders"); await expect(page.getByTestId("order-row")).toHaveCount(2); await expect(page.getByTestId("order-row").first()).toContainText("shipped");}); test("renders the error state on a server failure", async ({ page }) => { await page.route("**/api/orders", (route) => route.fulfill({ status: 500, contentType: "application/json", body: "{}" }) ); await page.goto("https://example.com/orders"); await expect(page.getByText("Could not load orders")).toBeVisible();});The first test intercepts every request matching **/api/orders and answers with a fabricated JSON payload, so the real API is never called and the row count is deterministic. The second test fulfills with status 500 to drive the error UI. Both run entirely offline and produce stable assertions.
In production
- Analytics, ad tags and third-party widgets are stubbed with route.abort() so they never cause flaky timings.
- Payment and external APIs (Stripe, shipping rates) are mocked with fixtures so tests are deterministic and cost nothing.
- CI runs an 'offline mode' suite that aborts all network requests except same-origin, proving the app degrades gracefully.
- Contract-style tests waitForResponse to the internal API and assert the request payload the UI actually sent.
Best practices
- Always register routes before the triggering action — typically before page.goto — so the request is caught.
- Match with globs or regexes, not full URLs that change between environments.
- Keep mock fixtures as JSON files or builder functions so the same stub is shared across tests.
- Don't mock everything: leave a few integration suites hitting the real backend to catch contract drift.
- For a few specific requests use page.route; for whole hosts (ads, analytics) use context.route.
Common mistakes
- Registering the route after the request already fired, then wondering why the mock didn't apply.
- Over-broad patterns like '**' that intercept static assets and break page rendering.
- Mocking the API but not the error path, so error-state code is never tested.
- Making mocks too clever with dynamic logic instead of a simple fixture, which hides bugs in the app.
Common issues
Route handler registered but the mock never appliesThe route was added after the request fired, or the pattern doesn't match the actual URL.
Register the route before goto and verify the URL with page.on('request') logging or the trace's network tab.
Request intercepted with route.continue() but never hits the networkBy default, continue() skips the network stack unless you pass route.continue({ }, { waitUntil: 'commit' }).
Accept default fast continue for most cases, or explicitly await route.fetch() + route.fulfill() when you need the real response.
ERR_CONNECTION_REFUSED on intercepted APIThe stub pattern accidentally also matched the app's own assets, or the handler aborted everything.
Narrow the glob (e.g. **/api/orders) and log matches before the handler acts.
Debugging tips
- Log intercepted URLs: add console.log inside the handler to confirm which requests match.
- Use page.on('request', ...) and page.on('response', ...) to inspect every network event in real time.
- Open the trace viewer and filter the network tab to see fulfilled vs continued requests.
- Set a breakpoint inside the route handler when running with npx playwright test --debug.
Interview checkpoint
Hands-on lab
- Intercept an external weather API with a JSON stub
- Simulate a 500 to test the error UI
- Simulate a slow network to test the loading state
- Assert the app made the expected request with waitForResponse
Mock the Weather API
35 min · intermediate
Quick quiz
Which call answers an intercepted request with a fake JSON response?
A route registered after page.goto() still intercepts the navigation request.
You need a test that verifies the app shows an error when its API is completely unreachable. What is the right mock?
Cheat sheet
route.continue()Pass through to the serverroute.fulfill({ status, body })Fabricated responseroute.abort()Fail the requestroute.fetch()Re-fetch then fulfill with real dataawait page.waitForResponse(url)Wait and inspect a responsepage.on('request', fn)Observe requests live**/api/ordersGlob: any host, this pathhttps://api.example.com/**Match one host/orders\d+$/.test(url)Regex predicate in handler**/*.{png,jpg}Match image assets