How to Intercept and Modify Network Requests and Responses in Zendriver

Zendriver provides an async context manager Tab.intercept() that wraps the Chrome DevTools Protocol Fetch domain, allowing you to pause, inspect, and modify HTTP traffic without handling raw CDP commands.

Intercepting and modifying network requests and responses is essential for mocking APIs, blocking trackers, or debugging frontend applications. Zendriver, a Python async driver for Chrome DevTools Protocol, exposes this capability through a high-level API built on top of the CDP Fetch and Network domains. This guide explains how to use Tab.intercept() to capture and manipulate network traffic using the actual implementation from the cdpdriver/zendriver repository.

Understanding the Interception Architecture

Zendriver's interception system operates through three distinct layers that abstract the complexity of Chrome DevTools Protocol.

User-Level API in Tab

The entry point for all interception functionality resides in zendriver/core/tab.py. The Tab.intercept() method creates an async context manager that returns a BaseFetchInterception object:


# zendriver/core/tab.py (lines 1216-1230)

async def intercept(self, url_pattern: str, request_stage: RequestStage, 
                   resource_type: Optional[ResourceType] = None):
    """Create an interception context for the given URL pattern."""
    interception = BaseFetchInterception(self, url_pattern, request_stage, resource_type)
    await interception._setup()
    return interception

Interception Helper Implementation

The BaseFetchInterception class in zendriver/core/intercept.py manages the CDP session lifecycle. It registers the Fetch.enable command with a specific RequestPattern, listens for Fetch.RequestPaused events, and provides high-level helper methods like continue_request(), fulfill_request(), and fail_request().

CDP Bindings Layer

Low-level CDP commands are defined in zendriver/cdp/fetch.py and zendriver/cdp/network.py. These generated modules provide Python dataclasses for RequestPattern, RequestStage, ResourceType, HeaderEntry, and command methods like Fetch.continueRequest and Fetch.fulfillRequest.

Setting Up Request Interception

To begin intercepting traffic, import the required CDP enums and create an interception context using the async with statement. The context manager automatically handles Fetch.enable on entry and Fetch.disable on exit.

import zendriver as zd
from zendriver.cdp.fetch import RequestStage
from zendriver.cdp.network import ResourceType

async def intercept_api_calls(tab: zd.Tab):
    # Intercept XHR responses matching the pattern

    async with tab.intercept(
        url_pattern="*/api/*", 
        request_stage=RequestStage.RESPONSE,
        resource_type=ResourceType.XHR
    ) as interception:
        await tab.get("https://example.com/dashboard")
        # Interception logic here

The url_pattern supports wildcards (*). The request_stage parameter accepts RequestStage.REQUEST (pause before sending to server) or RequestStage.RESPONSE (pause after server reply).

Inspecting and Modifying Requests

Once the Fetch.RequestPaused event fires, the interception object captures the event and exposes methods to inspect or alter the network exchange.

Reading Request Data

Access the original request details through the request property:

request = await interception.request
print(f"Method: {request.method}")
print(f"URL: {request.url}")
print(f"Headers: {request.headers}")

Modifying Request Parameters

To alter the request before it reaches the server, use continue_request() with override parameters:

await interception.continue_request(
    url="https://api.example.com/v2/data",  # Redirect to different endpoint

    method="POST",                          # Change method

    headers=[{"name": "Authorization", "value": "Bearer token123"}],
    post_data='{"modified": true}'
)

If you do not wish to modify the request, call continue_request() without arguments to proceed normally.

Handling Responses

Intercepting responses allows you to read body content, substitute mock data, or block unwanted resources entirely.

Reading Response Bodies

When intercepting at the RESPONSE stage, fetch the body using the response_body property. This internally calls Fetch.getResponseBody and handles base64 decoding:

body, is_base64 = await interception.response_body
if is_base64:
    import base64
    body = base64.b64decode(body).decode('utf-8')
print("Response content:", body)

Fulfilling with Custom Data

To provide a synthetic response without hitting the server, use fulfill_request(). This is useful for mocking APIs during testing:

import json
from zendriver.cdp.fetch import HeaderEntry

mock_data = json.dumps({"user": "test", "id": 123})
headers = [
    HeaderEntry(name="Content-Type", value="application/json"),
    HeaderEntry(name="Content-Length", value=str(len(mock_data)))
]

await interception.fulfill_request(
    response_code=200,
    response_headers=headers,
    body=mock_data
)

Blocking Requests

To abort a request entirely, intercept at the REQUEST stage and call fail_request():

from zendriver.cdp.network import ErrorReason

async with tab.intercept("*tracker.js", RequestStage.REQUEST, ResourceType.SCRIPT) as inter:
    await tab.get("https://example.com")
    # Immediately abort the request

    await inter.fail_request(ErrorReason.BLOCKED_BY_CLIENT)

Advanced Patterns

Reusing Interceptions Across Page Reloads

By default, an interception context captures a single RequestPaused event. To intercept multiple requests or survive page reloads, use the reset() method:

async with tab.intercept("*api/data", RequestStage.RESPONSE) as inter:
    await tab.get("https://example.com/page1")
    body1, _ = await inter.response_body
    await inter.continue_request()
    
    # Reset to capture the next occurrence

    await inter.reset()
    
    await tab.reload()
    body2, _ = await inter.response_body
    await inter.continue_request()

The reset() method clears the internal future, disables the fetch pattern, and re-enables it, allowing the same context to handle subsequent requests.

Summary

  • Zendriver exposes network interception through Tab.intercept(), an async context manager that wraps CDP Fetch domain commands.
  • The implementation resides in zendriver/core/intercept.py (BaseFetchInterception) and integrates with zendriver/cdp/fetch.py for low-level protocol bindings.
  • Use RequestStage.REQUEST to modify or block requests before they reach the server, and RequestStage.RESPONSE to inspect or substitute response bodies.
  • Key methods include continue_request() (modify and forward), fulfill_request() (mock responses), fail_request() (block requests), and response_body (read captured content).
  • The reset() method allows reusing the same interception context across multiple page loads or requests.

Frequently Asked Questions

How do I intercept only specific resource types like images or scripts?

Pass the resource_type parameter to tab.intercept() using values from zendriver.cdp.network.ResourceType. For example, use ResourceType.IMAGE to intercept only image requests or ResourceType.SCRIPT for JavaScript files. This filtering happens at the CDP level, so only matching requests trigger the RequestPaused event.

Can I modify both the request and response in a single interception?

No, you must choose the interception stage when setting up the context. Use RequestStage.REQUEST to modify headers, method, URL, or POST data before the request reaches the server. Use RequestStage.RESPONSE to read or substitute the response body after the server replies. If you need to handle both stages for the same URL, create two separate interception contexts.

What happens if I don't call continue_request or fulfill_request?

If you intercept a request and do not explicitly call continue_request(), fulfill_request(), or fail_request(), the browser tab will hang indefinitely waiting for the Fetch domain to resolve the paused request. Always ensure your interception logic reaches one of these resolution methods, ideally using try/finally blocks or context managers to guarantee cleanup.

How do I inspect response headers, not just the body?

The Fetch.RequestPaused event includes a response_headers field when intercepting at the RESPONSE stage. Access these through the interception object's internal event data. However, if you need to modify response headers specifically, use continue_response() (available when intercepting responses) to override headers while keeping the original body, or use fulfill_request() to provide completely custom headers and body content.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →