How to Send Custom Chrome DevTools Protocol (CDP) Commands with Zendriver
Zendriver exposes the full Chrome DevTools Protocol through the cdp package, allowing you to send any CDP command by passing generator objects from domain modules like cdp.page or cdp.runtime to the Tab.send() method.
The cdpdriver/zendriver library provides a Python interface to control Chrome browser instances programmatically. When you need functionality beyond the high-level API, you can send custom Chrome DevTools Protocol (CDP) commands directly using the built-in cdp package generators and the Tab.send method.
Understanding the CDP Architecture in Zendriver
The Generator-Based Command Pattern
Zendriver represents every CDP domain (e.g., page, runtime, dom) as a Python module containing functions that return generator objects. According to the source code in zendriver/core/connection.py (lines 38-44), these generators are understood by the low-level Connection.send implementation and forwarded to the browser via WebSocket.
This design means that calling cdp.runtime.evaluate() does not execute immediately—it returns a generator describing the command parameters and target method. The actual transmission happens when you pass this generator to a sending mechanism.
The Four-Step Execution Flow
The typical flow for sending custom CDP commands involves these components:
- Create a
Tabobject – This holds aConnectionthat manages a WebSocket to the Chrome target. - Call a CDP method – Invoking
cdp.page.navigate(url='...')returns a generator describing the CDP command. - Pass to
Tab.send–Tab.sendforwards the generator toConnection.send(see implementation inzendriver/core/tab.py, lines 48-66). - Handle the response –
Connection.send(lines 34-71 inzendriver/core/connection.py) registers the request, ensures the necessary domain is enabled viaConnection._register_handlers(lines 80-124), and awaits the decoded response or raisesProtocolExceptionon error.
Enabling Domains and Executing CDP Commands
Many CDP domains require explicit enablement before use. The following example demonstrates enabling the Runtime domain and evaluating JavaScript directly using raw CDP commands, as shown in docs/tutorials/tutorial-code/cdp-1.py.
import asyncio
import zendriver as zd
from zendriver import cdp
async def main() -> None:
browser = await zd.start()
tab = await browser.get("https://example.com")
# Enable the Runtime domain (required for many evaluate calls)
await tab.send(cdp.runtime.enable())
# Execute a custom CDP command – evaluate a script in the page context
result = await tab.send(
cdp.runtime.evaluate(
expression="document.title",
return_by_value=True,
await_promise=False,
)
)
print("Page title:", result[0].value) # result is a list of RemoteObjects
await browser.stop()
if __name__ == "__main__":
asyncio.run(main())
The cdp.runtime.enable() call returns a generator that activates the Runtime domain on the browser target. Subsequently, cdp.runtime.evaluate() constructs a generator describing the evaluation parameters, which tab.send transmits via the underlying Connection.send logic.
Sending Advanced Custom CDP Commands
Because Tab.send accepts any generator produced by the cdp package, you can issue commands not wrapped by high-level Zendriver helpers. This enables advanced scenarios such as custom network interception, performance tracing, or low-level DOM manipulation.
import asyncio
import zendriver as zd
from zendriver import cdp
async def main() -> None:
browser = await zd.start()
tab = await browser.get("https://example.com")
# Directly send the Page.navigate command with a URL of your choice
await tab.send(cdp.page.navigate(url="https://www.python.org"))
# Wait for the navigation to finish
await tab.wait_for_load_state("load")
await browser.stop()
if __name__ == "__main__":
asyncio.run(main())
Here, cdp.page.navigate() creates a generator for the Page.navigate command. By calling await tab.send(), you issue the navigation without relying on a dedicated high-level wrapper, while still benefiting from Zendriver's automatic target management and event handling.
Summary
- Zendriver exposes all CDP domains through the
cdppackage, where each method returns a generator object describing the specific protocol command. - Use
await tab.send()to execute any CDP command by passing generators from modules likecdp.page,cdp.runtime, orcdp.dom. - The underlying
Connection.sendmethod inzendriver/core/connection.py(lines 34-71) handles WebSocket transmission, automatic domain enablement, and response decoding. - This architecture allows you to mix high-level Zendriver APIs with low-level CDP commands for maximum flexibility in browser automation.
Frequently Asked Questions
What is the difference between Tab.send and Connection.send?
Tab.send provides the public API for sending CDP commands from a specific browser tab, implemented in zendriver/core/tab.py (lines 48-66). It forwards generators to Connection.send, which contains the core WebSocket logic in zendriver/core/connection.py (lines 38-44 and 34-71) that actually transmits the command to Chrome and manages the response lifecycle.
Do I need to enable CDP domains manually before sending commands?
Yes, many CDP domains require explicit enablement before use. The Connection._register_handlers method (lines 80-124 in zendriver/core/connection.py) manages domain state, but you should explicitly call enable methods like cdp.runtime.enable() or cdp.network.enable() before issuing commands that depend on those domains to ensure the browser target is ready to accept them.
Can I mix custom CDP commands with Zendriver's high-level API?
Absolutely. Since Tab.send works with any CDP generator while high-level methods use the same underlying infrastructure, you can interleave raw CDP commands with conveniences like tab.wait_for_load_state() or browser.get() within the same script without conflicts.
Where are the CDP domain modules defined?
The cdp package contains automatically generated Python modules for each CDP domain (e.g., zendriver/cdp/page.py, zendriver/cdp/runtime.py). These modules define functions that return generators describing the specific CDP protocol commands, their parameters, and return types according to the Chrome DevTools Protocol specification.
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 →