Senior 3 min · March 15, 2026

Playwright Network Mocking — Mock Response Shape Mismatch

Frontend renders no data; console shows 'undefined' errors because mock response keys don't match TypeScript interfaces.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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%.
Plain-English First

Think of network mocking like a stunt double for your backend. Instead of your browser talking to the real server, Playwright replaces the conversation with a pretend server that returns exactly what you want. This lets you test your UI in any scenario — success, failure, slow network — without waiting for the real server to be ready.

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).
ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.
Middleware Chain Analogy
  • 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.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
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.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
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.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
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.

ExamplePYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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.
● Production incidentPOST-MORTEMseverity: high

Mock Response Shape Mismatch

Symptom
Frontend renders no data after navigation. Console shows multiple 'undefined' errors in JavaScript.
Assumption
Engineer assumed the mock response keys matched the API contract exactly.
Root cause
Mock response object keys did not align with the TypeScript interfaces used in the frontend. The UI accessed 'user_name' but the mock returned 'username'.
Fix
Manually 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 intercepted4 entries
Symptom · 01
Request not intercepted (bypasses route handler)
Fix
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.
Symptom · 02
Mock response causes JavaScript errors in the console
Fix
Inspect 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.
Symptom · 03
Test execution is slow despite having mocks
Fix
Ensure you abort unnecessary resource requests — images, fonts, analytics scripts. Add page.route('*/.{png,jpg,gif,svg,woff2}', lambda r: r.abort()) before any navigation.
Symptom · 04
Mock is applied in one page but not in another
Fix
Routes 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.
★ Quick Reference for Network Mocking DebuggingCommands and patterns for diagnosing interception issues in Playwright Python.
Request not intercepted
Immediate action
Enable 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 now
Wrap 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 action
Open 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 now
Copy 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 action
Reduce 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 now
Abort heavy assets early in the test setup — add a global route to abort images/fonts before any navigation.
Network Interception Strategies
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

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

Common mistakes to avoid

4 patterns
×

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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Playwright's network interception differ from using a library l...
Q02SENIOR
Your test suite is slow because it loads heavy 4K hero images. How would...
Q03SENIOR
How do you verify that an application sent the correct JSON payload to a...
Q04SENIOR
Explain the difference between `route.fulfill` and `route.continue`. In ...
Q05SENIOR
How would you simulate a Gateway Timeout (504) for a specific backend se...
Q01 of 05SENIOR

How does Playwright's network interception differ from using a library like 'Responses' or 'HTTPretty' in Python?

ANSWER
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I mock WebSocket connections with Playwright?
02
How do I mock a specific API but let all others pass through?
03
Will mocked responses show up in the Playwright Trace Viewer?
04
Can I use regex patterns with route()?
05
How do I mock a response for a specific HTTP method (GET vs POST)?
🔥

That's Python Libraries. Mark it forged?

3 min read · try the examples if you haven't

Previous
Playwright Python — Browser Automation and Testing
24 / 51 · Python Libraries
Next
NumPy Broadcasting — How It Actually Works