Playwright’s network interception lets you see and manipulate HTTP requests and responses as they happen, which is incredibly powerful for testing.

Here’s a Playwright script intercepting and mocking a GET request to /api/users:

from playwright.sync_api import sync_playwright

def run(playwright):
    browser = playwright.chromium.launch()
    context = browser.new_context()
    page = context.new_page()

    # Intercept requests to the specific API endpoint
    page.route("**/api/users", lambda route: route.fulfill(
        status=200,
        content_type="application/json",
        body='[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]'
    ))

    # Navigate to a page that makes the API call
    # Assume this page makes a fetch or XHR to /api/users
    page.goto("http://localhost:3000/my-app") # Replace with your actual app URL

    # You can now assert that the data from the mocked response is displayed
    page.wait_for_selector("text=Alice")
    page.wait_for_selector("text=Bob")

    browser.close()

with sync_playwright() as p:
    run(p)

This script sets up a route handler before navigating to the page. When the page requests /api/users, Playwright intercepts it and serves the predefined JSON response instead of making a real network call. This allows the test to proceed as if the API call succeeded, even if the actual API is down or not yet implemented.

The core problem Playwright’s network interception solves is the inherent flakiness and dependency of end-to-end tests. Without it, your tests are at the mercy of external services. A test might fail because the backend API is slow, returns an unexpected error, or is simply unavailable. Network interception decouples your frontend tests from these backend realities. You can simulate successful responses, error conditions (like 404s or 500s), or even slow responses to test how your UI handles them.

Internally, Playwright operates as a proxy. When you call page.route(), you’re telling Playwright’s browser automation engine to intercept network traffic originating from the browser context. For each matching request, Playwright executes your provided callback function. This callback receives a Route object, which has methods like fulfill() to provide a custom response or continue() to let the original request proceed to the network.

The key levers you control are the url pattern in page.route() and the parameters to route.fulfill(). The url can be a string, a regular expression, or a glob pattern, allowing fine-grained control over which requests are intercepted. fulfill() accepts status, headers, content_type, and body to construct the mocked response. You can also use route.request.method(), route.request.post_data(), and other properties of the route.request object within your callback to make decisions about how to mock the response based on the incoming request details.

When mocking API responses, it’s common to need to intercept requests for specific resources like images or stylesheets. You might think you need a separate page.route() call for each. However, Playwright’s routing is powerful enough to handle this with a single, more general route. If you have a route like page.route("**/*.css", lambda route: route.fulfill(status=200, content_type="text/css", body="")), it will intercept all CSS files and return an empty response. This is particularly useful for speeding up tests by preventing the download of non-essential assets.

The next concept you’ll likely explore is how to handle multiple, sequential API calls and ensure they are mocked in the correct order for complex user flows.

Want structured learning?

Take the full Playwright course →