Playwright Request Interception Without Hiding Real Failures
Learn Playwright request interception with route, fulfill, continue, abort, strict matching, waitForResponse, and a measured false-pass example.
Playwright request interception lets a test observe or control the network call a browser page makes. Register page.route() or browserContext.route() before the triggering action, match the smallest useful request surface, inspect the actual method, URL, headers, or body, and then resolve the route exactly once with fulfill, continue, abort, or fallback.
The most important rule is not syntactic. A mock must fail when the application breaks the contract you meant to test. A broad handler that fulfills every matching URL with 200 can make the wrong HTTP method, query, tenant, or payload look healthy.
This guide shows the core Playwright route methods, when to observe instead of intercept, how to avoid common races, and a controlled experiment where a loose mock hid the same request regression in every run.
Choose observation or interception first
Use observation when the real request should happen and the test only needs to verify it. page.waitForResponse() and the request and response events do not replace the response.
Use interception when the test must change the outcome: create a deterministic fixture, simulate an error, remove a noisy dependency, rewrite a request, or modify a real response.
Goal | Playwright API | Effect on the request |
|---|---|---|
Verify a real response after an action |
| Observes without replacing it |
Return controlled data |
| Responds from the test |
Send the request to the real server |
| Passes through, with optional changes |
Simulate a network failure |
| Stops the request with a network error |
Let another matching handler decide |
| Continues through the route-handler chain |
Fetch the real response and edit it |
| Calls the server, then returns a modified response |
Do not use a mock merely because it is convenient. If the test is meant to prove that the browser and server still agree, replacing the server response removes half of that contract.
Observe a request with waitForResponse
Create the response promise before the click. Awaiting it before the action would deadlock because the request has not started.
This answers whether the real browser request returned the expected result. It does not create a fake response.
A public Stack Overflow question about validating two calls after one click shows why this distinction matters in practice. The reporter needed both response statuses and the second request payload. In Playwright, keep both waits ready before the click, await both afterward, and inspect the request behind a response with response.request().
Fulfill only the contract you intend to mock
A route pattern decides which URLs enter the handler. Your handler should decide whether each request really matches the contract.
The request still reaches the real server when the method or query is wrong. That is useful when a wrong request should expose a 400, 405, or authentication failure instead of receiving fixture data.
An alternative is to throw immediately on a mismatch. Choose based on the test's purpose:
Continue the mismatch when the real server response is part of the diagnosis.
Throw on the mismatch when the route contract itself is the assertion.
Fallback when a broader fixture-level handler should get the next chance to process it.
Know what each route method proves
route.fulfill returns a response from the test
Use fulfill for controlled success, empty, permission, rate-limit, or server-error states. Set the status and content type explicitly so the fixture resembles the contract the page expects.
A fulfilled request does not prove the real API works. It proves the page handled the response your test supplied.
route.continue reaches the network
Use continue to pass through unchanged or to modify the outgoing URL, method, headers, or body. The official Route documentation notes that continue sends the request immediately, so other matching route handlers do not run.
Avoid silently repairing application defects in the handler. If the page sends POST but the endpoint requires GET, changing the method to GET can turn a regression into a green test.
route.abort simulates a network failure
Use abort when the user-facing behavior depends on a true network failure, such as a refused connection or disconnected internet. An HTTP 500 is different: the browser received an HTTP response, so return it with fulfill({ status: 500 }) when that is the state you need.
route.fallback hands control to another handler
Playwright runs several matching routes in reverse registration order. fallback passes the request to the next matching handler. This is useful when a test-specific override should sit above a shared default without swallowing unrelated methods.
route.fetch modifies a real response
Use route.fetch() when the real server should still run but the test needs to change one field or header before the page receives the response.
This is not a pure unit fixture. Availability, authentication, and data from the real endpoint can still affect the test.
A loose route produced a false green result five times
We built a synthetic release-review page with no customer data and no external side effects. The endpoint /api/release-flags?release=42 accepts GET. The page contains a deliberate regression and sends POST when the tester clicks Load release flags.
We ran two route policies in Playwright 1.63.0 with bundled Chromium. Each policy ran five times in a fresh browser context with service workers blocked.
Route policy | Actual method | Route decision | Response | Visible result | Repetitions |
|---|---|---|---|---|---|
Loose URL-only mock |
| Fulfill fixture | 200 | Ready to publish | 5 |
Strict method, path, and query check |
| Continue to server | 405 | Load failed HTTP 405 | 5 |
The loose handler returned fixture data for the broken POST, so a visible assertion for Ready to publish would pass in all five runs. The strict handler refused to mock the wrong method, and the real endpoint exposed the 405 in all five runs.
This is a controlled example, not a claim about the failure rate of Playwright suites. It demonstrates a specific risk: URL matching alone can replace a response for a request whose contract has already drifted.
The fix is not to avoid request interception. The fix is to make the mock's boundary explicit and preserve enough information for another engineer to see why it ran.
Record the route decision as part of the test result
When a network mock decides the outcome, log or attach the facts that make its decision reviewable:
Attach that bounded log on failure rather than dumping every request, header, and response body. Network evidence can contain tokens, private query values, account identifiers, or customer data. Keep only what the teammate opening the test result needs, and redact secrets before sharing.
For a broader failure timeline, open the Playwright Trace Viewer workflow for finding the first mismatch. For visual state, use Playwright screenshots that help debug failures. Neither should substitute for an exact request-contract assertion.
Handle service workers deliberately
If route handlers or network events appear to miss requests, check whether a service worker owns the request. Playwright's Network documentation recommends serviceWorkers: 'block' when using native routing and missing events are caused by a service-worker mock such as MSW.
Blocking service workers makes interception more predictable, but it also changes the browser environment. If the product's offline or cache behavior is under test, allow the worker and use browser-context events and routing that account for worker-owned requests. Record that choice in the test result so the receiver knows which browser path actually ran.
Review every interception with this checklist
Register before the trigger. Create routes and response waits before navigation or the click that starts the request.
Match method and URL. Include the path, method, and decisive query or operation name.
Inspect the payload when it matters. A correct endpoint with the wrong tenant or object ID is still wrong.
Resolve every matched route once. Fulfill, continue, abort, or fallback. Do not leave a request hanging.
Assert the page outcome too. A
200does not prove the browser displayed or saved the right state.Keep one real contract test. A mocked suite should not be the only check that the browser and API agree.
Control service workers intentionally. Block them for predictable routing or test them as part of the product behavior.
Preserve a bounded decision log. Give the teammate the trigger, actual request, route decision, response, and visible result.
Remove shared handlers cleanly. Keep route setup isolated so one test's policy does not leak into another.
Review generated tests. If Codegen or an assistant produced the flow, apply the same contract checks before commit. Our Playwright Codegen review guide covers the wider review.
The practical rule
Intercept the narrowest contract that creates the state you need. Observe the real network when replacing it would hide the thing you are trying to prove. Then preserve the route decision alongside the browser action and visible result.
When a failure depends on the ordered browser path rather than one automated test, the team behind Samelogic built CSS Selector & XPath Finder so a permitted person can deliberately start capture, play the steps back, jump to the failing step, and send the bounded context to the teammate fixing the issue. It is not passive session replay and does not replace Playwright's network controls.
Sources
Related workflows
Move from editorial context into the selector, Playwright, and bug-reproduction pages that turn exact UI evidence into action.

