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 withzendriver/cdp/fetch.pyfor low-level protocol bindings. - Use
RequestStage.REQUESTto modify or block requests before they reach the server, andRequestStage.RESPONSEto inspect or substitute response bodies. - Key methods include
continue_request()(modify and forward),fulfill_request()(mock responses),fail_request()(block requests), andresponse_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →