Skip to content
Home Python Advanced Network Interception and Mocking in Playwright Python

Advanced Network Interception and Mocking in Playwright Python

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Libraries → Topic 24 of 51
Master network mocking and request interception in Playwright Python.
🔥 Advanced — solid Python foundation required
In this tutorial, you'll learn
Master network mocking and request interception in Playwright Python.
  • Use page.route for fine-grained control over individual HTTP requests based on URL patterns.
  • Mocking allows you to test frontend components without a working backend, drastically increasing test stability.
  • Aborting unnecessary requests (like heavy images or analytics) can reduce test execution time by up to 40%.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • page.route() intercepts HTTP requests before they leave the browser.
  • route.fulfill() returns a fake response with custom status, headers, and body.
  • route.continue() lets the request proceed, optionally modifying headers or payload.
  • route.abort() blocks unwanted requests (trackers, images, fonts).
  • Mocking eliminates backend flakiness — tests become deterministic and fast.
  • Performance: aborting heavy assets can cut test execution time by up to 40%.
🚨 START HERE
Quick Reference for Network Mocking Debugging
Commands and patterns for diagnosing interception issues in Playwright Python.
🟡Request not intercepted
Immediate ActionEnable Playwright's verbose logging to see interception decisions
Commands
page.route('**/api/users', lambda r: r.fulfill(status=200, body='[]'))
context.tracing.start(snapshots=True, screenshots=True, sources=True)
Fix NowWrap route registration in a try/finally and assert page.locator('text=expected').is_visible() after navigation to verify mock was applied.
🟡Mock response causes blank page
Immediate ActionOpen Trace Viewer: playwright show-trace trace.zip
Commands
page.route('**/api/data', handler) with a print('Intercepted:', route.request.url) for debugging
route.fulfill(headers={'Content-Type': 'application/json'}, body='{"key":"value"}')
Fix NowCopy the real response body from browser DevTools Network tab and use it as the mock body — then shape it.
🟡Test times out waiting for network idle
Immediate ActionReduce timeouts: page.set_default_timeout(5000)
Commands
page.route('**/*', lambda r: r.continue_()) to see all requests being made
page.goto(url, wait_until='domcontentloaded') instead of 'networkidle'
Fix NowAbort heavy assets early in the test setup — add a global route to abort images/fonts before any navigation.
Production IncidentMock Response Shape MismatchA mock returned field 'username' but the UI expected 'user_name' — entire page rendered blank with no obvious error.
SymptomFrontend renders no data after navigation. Console shows multiple 'undefined' errors in JavaScript.
AssumptionEngineer assumed the mock response keys matched the API contract exactly.
Root causeMock response object keys did not align with the TypeScript interfaces used in the frontend. The UI accessed 'user_name' but the mock returned 'username'.
FixManually compare mock response structure against real API response (use browser devtools network tab). Use the same serialisation library if possible. For TypeScript projects, generate mock data from the same type definitions.
Key Lesson
Always align mock response structure exactly with the API contract.Use TypeScript types or OpenAPI specs as the source of truth for mock shapes.Add a test that validates mock response against the real API schema — catch mismatches early.
Production Debug GuideWhen your mock isn't working or requests aren't being intercepted
Request not intercepted (bypasses route handler)Check URL pattern: use '*/api/' for glob, or regex with r'pattern'. Playwright uses first-match wins — a broader pattern may capture the request before your specific handler.
Mock response causes JavaScript errors in the consoleInspect the Trace Viewer (playwright show-trace trace.zip) to see exactly what the browser received. Verify mock JSON fields match the expected interface. Use the real API response as a starting template.
Test execution is slow despite having mocksEnsure you abort unnecessary resource requests — images, fonts, analytics scripts. Add page.route('*/.{png,jpg,gif,svg,woff2}', lambda r: r.abort()) before any navigation.
Mock is applied in one page but not in anotherRoutes are scoped to the page where they are registered. Use context.route() to apply a route handler to all pages in the same browser context.

Testing a modern frontend often feels like a hostage situation — you are at the mercy of the backend's availability and speed. Playwright's network interception capabilities break this dependency.

By leveraging the route API, you can turn your browser automation into a powerful mocking engine. Whether you need to simulate a 500 Internal Server Error, mock a slow 3G connection for performance testing, or entirely replace a JSON response to test a specific UI state, Playwright handles it natively via the DevTools protocol. This guide moves beyond basic automation into advanced network manipulation strategies for production-grade test suites.

The Core Concept — page.route()

In Selenium, mocking network traffic usually required a separate proxy like BrowserMob. In Playwright, you simply define a routing rule. When the browser makes a request that matches your pattern (URL or Glob), Playwright intercepts it and gives you control over the route object.

You have three main options once a request is intercepted
  • route.fulfill(): Provide a mock response (status, headers, body).
  • route.continue(): Let the request proceed to the internet (possibly with modifications).
  • route.abort(): Kill the request (useful for blocking ads or analytics).
Example · PYTHON
12345678910111213141516171819
from playwright.sync_api import sync_playwright

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

    # Intercept all calls to the user profile API
    page.route("**/api/v1/user/profile", lambda route: route.fulfill(
        status=200,
        content_type="application/json",
        body='{"id": 1, "username": "forge_admin", "role": "superuser"}'
    ))

    page.goto("https://example.com/dashboard")
    # The UI now sees 'forge_admin' regardless of the actual database state
    browser.close()

with sync_playwright() as p:
    run(p)
▶ Output
API request intercepted and fulfilled with mock data.
Mental Model
Middleware Chain Analogy
Think of route handlers like Express.js middleware — each request passes through the first matching handler.
  • Patterns are checked in order of registration
  • First match wins, so define specific routes before generic fallbacks
  • You can chain multiple handlers by calling route.fallback() in advanced scenarios
📊 Production Insight
Engineers often register routes after navigation — they never fire.
Register all routes before page.goto().
Rule: routes are lazy — they intercept the next matching request, not past ones.
🎯 Key Takeaway
page.route() is the foundation.
Master fulfill, continue, abort — each serves a distinct purpose.
Pattern ordering matters: specific before generic.
Which Route Action Should You Use?
IfYou need to return fake data for a specific API call
UseUse route.fulfill() with a realistic JSON body.
IfYou want to add a header or modify payload without blocking
UseUse route.continue() with modified headers/body.
IfYou want to block analytics, images, or ads
UseUse route.abort() — no response sent.

Simulating Edge Cases: API Failures and Timeouts

One of the hardest things to test is how your UI behaves when the server dies. Using Playwright, you can force a 500 error or even a total connection failure to ensure your error handling actually works.

Example · PYTHON
123456789101112
def test_error_handling(page):
    # Simulate a server crash for the login endpoint
    page.route("**/api/login", lambda route: route.fulfill(
        status=500,
        body="Internal Server Error"
    ))

    page.goto("https://example.com/login")
    page.get_by_role("button", name="Sign In").click()

    # Verify the UI shows the correct error message
    expect(page.get_by_text("Server is currently unavailable")).to_be_visible()
▶ Output
UI error state verified successfully.
⚠ Don't Forget Timeouts
When mocking failures, ensure your UI doesn't hang forever on a pending request. Use page.set_default_timeout(5000) to fail fast if the response doesn't come.
📊 Production Insight
Mocking a 500 is easy, but intermittent failures (slow 504, malformed 200) are harder.
Engineers often forget to test the 'no response' scenario — route.abort() simulates a dropped connection.
Rule: cover at least three states: success, error, and no response.
🎯 Key Takeaway
Test errors, timeouts, and no-response scenarios.
route.abort() simulates dropped connections.
Cover all three failure modes for robust error handling.
What Failure State Are You Testing?
IfServer returns a specific HTTP error (500, 403)
UseUse route.fulfill() with the appropriate status code.
IfServer never responds (timeout)
UseUse route.fulfill() with a very long delay or call route.abort() after a timeout.
IfServer returns malformed or empty response body
UseUse route.fulfill() with an empty body or invalid JSON.

Advanced: Modifying Outgoing Requests

Sometimes you don't want to mock the whole response, but you need to inject a specific header (like an Auth token) or modify a payload before it leaves the browser. Use route.continue() for this.

Example · PYTHON
123456789
def test_header_injection(page):
    def handle_route(route):
        headers = route.request.headers
        headers["X-Test-Source"] = "Automated-Test"
        # Continue the request with the new header
        route.continue_(headers=headers)

    page.route("**/*", handle_route)
    page.goto("https://example.com")
▶ Output
Headers modified for all outgoing requests.
🔥Mutable vs Immutable Headers
route.request.headers is a dict-like object. Modifying it and passing to route.continue_() works, but some headers (Content-Length, Host) are read-only. Playwright will warn if you try to override them.
📊 Production Insight
Modifying headers is powerful, but beware of CORS preflight — modified headers may cause the browser to send an OPTIONS request.
If the mock doesn't handle OPTIONS, the actual request fails.
Rule: add a catch-all route for OPTIONS requests when modifying headers.
🎯 Key Takeaway
route.continue() modifies requests without mocking responses.
Watch for CORS preflight side effects.
Use this to inject auth tokens or test-specific headers.

Performance Testing: Mocking Slow Networks

Playwright's browser_context lets you simulate network conditions like offline mode or specific throughput limits — useful for testing how your app degrades gracefully.

Example · PYTHON
123456789
async def test_slow_network(browser):
    # Create a context that simulates an offline device
    context = await browser.new_context(offline=True)
    page = await context.new_page()

    try:
        await page.goto("https://example.com")
    except:
        print("Successfully verified offline behavior")
▶ Output
Successfully verified offline behavior
💡Network Throttling
Use browser_context.new_context(extra_http_headers={...}) to simulate specific network conditions. For realistic slow 3G, combine route interception with artificial delays via route.fulfill with a sleep.
📊 Production Insight
Offline mode only tests complete disconnect — it doesn't simulate slow but working connections.
For slow networks, use a custom route handler that adds a delay: asyncio.sleep(2) before fulfill.
Rule: test both offline (abort) and slow (delay) to cover user experience fully.
🎯 Key Takeaway
Network simulation covers offline and slow states.
route.fulfill with artificial delay mimics slow networks.
Combine with route.abort for complete offline testing.

Asserting Network Activity: Testing That Requests Were Made

Sometimes you don't need to mock a request — you just need to verify that a specific API call was made with certain parameters. Playwright provides page.wait_for_request() and page.wait_for_response() for this. These are critical for confirming analytics events, logging calls, or form submissions actually fired correctly.

You can combine these with route interception to validate both the outgoing request and the incoming response.

Example · PYTHON
1234567891011121314
# io.thecodeforge.playwright.network.assertions
from playwright.sync_api import expect

def test_analytics_event_fired(page):
    page.route("**/api/analytics", lambda route: route.continue_())  # allow through

    with page.expect_request("**/api/analytics") as request_info:
        page.get_by_role("button", name="Submit").click()

    request = request_info.value
    assert request.method == "POST"
    assert "/api/analytics" in request.url
    payload = request.post_data_json
    assert payload["event"] == "form_submit"
▶ Output
Analytics request verified successfully.
🔥expect_request vs wait_for_request
page.expect_request() is a context manager that waits for the request to be made and captures it. It's cleaner than page.wait_for_request() for most test scenarios.
📊 Production Insight
Engineers often assert the mock response but forget to verify the request was actually made.
If a route.abort() is too aggressive, the request never reaches the mock handler.
Rule: always use expect_request in a with block when testing request-driven functionality.
🎯 Key Takeaway
Mocking is not enough — verify requests were actually sent.
Use expect_request for clean assertion of outgoing calls.
Combine with explicit route patterns to avoid false positives.
When to Assert Network Activity
IfYou need to verify an API call was made with correct payload
UseUse page.expect_request() to capture the outgoing request.
IfYou need to verify the response data was received
UseUse page.expect_response() to capture the response (mocked or real).
IfYou want to ensure no extra requests were made
UseUse page.on('request', handler) to count requests and assert no unexpected ones.
🗂 Network Interception Strategies
When to use each route action
StrategyBest ForComplexity
route.abort()Blocking ads, tracking, or images to speed up testsLow
route.fulfill()Testing UI states with specific API dataMedium
route.continue()Modifying headers or simulating specific query paramsMedium
Request Monitoring (expect_request)Verifying that an analytics event was actually sentHigh
Combined routes (fulfill + abort)Full control over mock data and performance simultaneouslyHigh

🎯 Key Takeaways

  • Use page.route for fine-grained control over individual HTTP requests based on URL patterns.
  • Mocking allows you to test frontend components without a working backend, drastically increasing test stability.
  • Aborting unnecessary requests (like heavy images or analytics) can reduce test execution time by up to 40%.
  • Always prefer fulfilling with a real JSON structure rather than an empty string to avoid unexpected JavaScript 'undefined' errors.
  • Assert outgoing requests with expect_request to verify that the right data was sent.
  • Test at least three failure modes: HTTP error, timeout, and no response.

⚠ Common Mistakes to Avoid

    Using hardcoded full URL instead of glob pattern
    Symptom

    Interception works on local dev environment but fails in CI or on different deployments where the base URL changes.

    Fix

    Use glob patterns like '*/api/' or regex r'.api.' to match regardless of domain. Avoid hardcoding 'https://staging.example.com/api/v1/...'.

    Forgetting to register routes before page navigation
    Symptom

    The request goes out before the route handler is attached, so the mock is never applied.

    Fix

    Always call page.route() before page.goto() or before the action that triggers the request. Routes are attached per-page and only intercept future requests.

    Mocking a response with incorrect Content-Type
    Symptom

    The browser rejects the response as invalid, leading to console errors like 'Unexpected token < in JSON at position 0'.

    Fix

    Set the Content-Type header in route.fulfill() to match the expected MIME type, e.g., content_type='application/json'. Use the browser's network panel to copy the original response headers.

    Overusing route.continue() without proper handling
    Symptom

    The test becomes slow because many requests are processed unnecessarily, or the browser gets confused by unexpected header modifications.

    Fix

    Only use route.continue() when you need to modify the request. For most test scenarios, route.fulfill() or route.abort() is sufficient. Reduce the glob pattern scope to avoid intercepting every resource.

Interview Questions on This Topic

  • QHow does Playwright's network interception differ from using a library like 'Responses' or 'HTTPretty' in Python?SeniorReveal
    Playwright intercepts at the browser level via the Chrome DevTools Protocol, so it controls all HTTP traffic from the browser — including fetch, XHR, and even navigation requests. 'Responses' and 'HTTPretty' patch Python's HTTP client libraries (like requests), so they only work for server-side Python code. Playwright's approach is transparent to the JavaScript code running in the browser, making it ideal for end-to-end testing of frontend applications.
  • QYour test suite is slow because it loads heavy 4K hero images. How would you solve this using Playwright routes?Mid-levelReveal
    Use route.abort() to block image requests. Define a route that matches image file extensions: page.route('*/.{png,jpg,gif,svg,webp}', lambda r: r.abort()). This stops the browser from downloading images entirely, dramatically reducing network load and test time. For a more selective approach, you can also replace images with a small placeholder by using route.fulfill() with a 1x1 pixel base64 image.
  • QHow do you verify that an application sent the correct JSON payload to a /log endpoint without actually writing to the database?Mid-levelReveal
    Use page.expect_request() to capture the request before it reaches the server. Register a route for the /log endpoint that either continues or aborts, but wrap the action in a with block to capture the request object. Then assert on request.post_data_json (or request.headers) to verify the payload values. This verifies the behavior without side effects.
  • QExplain the difference between route.fulfill and route.continue. In what real-world scenario would you use both together?SeniorReveal
    route.fulfill() creates a completely fake response — the browser never talks to the server. route.continue() lets the request go through to the server but allows you to modify headers or the request body before it leaves. You might use both together in a rate-limiting simulation: for the first request, use continue() to let it pass, and for subsequent ones, fulfill() with a 429 status to simulate throttling.
  • QHow would you simulate a Gateway Timeout (504) for a specific backend service?SeniorReveal
    Use route.fulfill() with status=504 and an appropriate body. However, to simulate a real timeout where the server never responds, you can use a combination of route.fulfill() with a very long artificial delay (asyncio.sleep(60) in an async handler) or simply call route.abort() after a page timeout. The most realistic approach: in the route handler, sleep for longer than the test timeout, causing a timeout error — then verify your UI shows the correct gateway timeout message.

Frequently Asked Questions

Can I mock WebSocket connections with Playwright?

Currently, Playwright's route API is designed for HTTP/HTTPS. While you can monitor WebSockets via page.on('websocket'), full interception and mocking of WebSocket frames require low-level CDP (Chrome DevTools Protocol) commands. For most end-to-end tests, you can mock the initial HTTP upgrade handshake and then leave the WebSocket flow as-is.

How do I mock a specific API but let all others pass through?

Playwright is 'first match wins'. You define a specific route for your API (e.g., **/api/v1/target) and don't define routes for others. All unrouted requests will proceed to the network naturally. If you also have a broad route (like for analytics), make sure the specific API route is registered first.

Will mocked responses show up in the Playwright Trace Viewer?

Yes. The Trace Viewer records mock responses, including status, headers, and body. This makes it easy to debug exactly what data the UI received during a test failure. You can open the trace with playwright show-trace trace.zip and inspect each network request.

Can I use regex patterns with route()?

Yes, page.route() accepts a string glob or a compiled regex object. Example: page.route(re.compile(r'https?://api\.example\.com/v2/.*'), handler). Regex gives you more precision for complex patterns.

How do I mock a response for a specific HTTP method (GET vs POST)?

Inside your route handler, check route.request.method. You can conditionally call fulfill, continue, or abort based on method. Example: if route.request.method == 'POST': route.fulfill(...) else: route.continue_().

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousPlaywright Python — Browser Automation and TestingNext →NumPy Broadcasting — How It Actually Works
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged