Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chrome DevTools Protocol

Chrome DevTools Protocol (CDP) is the lower-level inspection and control API that Chromium-based browsers expose for debugging, profiling, and automation. It predates W3C WebDriver BiDi and only works on Chromium browsers (Chrome, Edge, Brave, Opera, …) — but it covers a large surface area that WebDriver itself doesn’t, including network throttling, fine-grained DOM inspection, runtime JS evaluation with object handles, request interception, device emulation, and more.

thirtyfour ships typed bindings for the most-used CDP commands so you can call them like normal Rust async methods, plus an optional WebSocket-based session for event subscription (network requests firing, lifecycle events, console messages, …).

When To Use CDP vs BiDi

You want…Use
Cross-browser support (Chrome and Firefox)BiDi
Rich Chromium-only features (full Network, Fetch, DOM)CDP
W3C-standard, future-proof bidirectional protocolBiDi
To call any of Network.*, Fetch.*, Runtime.*, etc.CDP
To resolve a WebElement to a CDP RemoteObjectIdCDP

Both can be used in the same session. CDP is on by default; BiDi opts in via a capability and a feature flag — see the BiDi chapter for details.

Quick Start

CDP is enabled by the cdp feature, which is on by default. Reach the handle via WebDriver::cdp():

use thirtyfour::cdp::domains::network::{ConnectionType, NetworkConditions};
use thirtyfour::prelude::*;

#[tokio::main]
async fn main() -> WebDriverResult<()> {
    let driver = WebDriver::managed(DesiredCapabilities::chrome()).await?;

    // Domain-grouped typed methods.
    let info = driver.cdp().browser().get_version().await?;
    println!("Chrome: {}", info.product);

    // Throttle the network like Chrome DevTools' "Slow 3G".
    driver.cdp().network().emulate_network_conditions(NetworkConditions {
        offline: false,
        latency: 200,
        download_throughput: 256 * 1024,
        upload_throughput: 64 * 1024,
        connection_type: Some(ConnectionType::Cellular3G),
    }).await?;

    driver.cdp().network().clear_browser_cache().await?;

    driver.quit().await
}

driver.cdp() is cheap to call — it just clones the underlying Arc<SessionHandle>. There is no separate connection: typed commands flow over the WebDriver vendor endpoint goog/cdp/execute, so they work on any session backed by a Chromium driver (chromedriver, msedgedriver, Brave’s driver, …) and even through Selenium Grid.

Typed Commands

Every command in cdp::domains is a Rust struct that pairs the request type with its response type and the wire method name. The domain facades on Cdp (browser(), network(), page(), …) wrap the most common ones in convenient async methods:

DomainExamples
browserget_version
pagenavigate, reload, get_frame_tree, capture_screenshot
networkenable, clear_browser_cache, set_extra_http_headers, emulate_network_conditions
fetchenable, continue_request, fail_request, fulfill_request
runtimeevaluate, call_function_on, enable, disable
domdescribe_node, get_box_model, query_selector
emulationset_device_metrics_override, set_user_agent_override
inputdispatch_mouse_event, dispatch_key_event
targetget_targets, attach_to_target, detach_from_target
storageclear_data_for_origin, get_cookies
logenable, clear
performanceenable, get_metrics

For the full list of fields on each command and response, see the thirtyfour::cdp::domains API docs.

The Untyped Escape Hatch

For one-off commands not in the curated set, use Cdp::send_raw. The CDP protocol viewer is the canonical reference for method names and parameter shapes — every domain has its own page (e.g. Browser, Page, Network).

#![allow(unused)]
fn main() {
use thirtyfour::prelude::*;
async fn run(driver: WebDriver) -> WebDriverResult<()> {
// No-arg command: pass `()` for the params.
let info = driver.cdp().send_raw("Browser.getVersion", ()).await?;
println!("user agent: {}", info["userAgent"]);

// With params:
driver.cdp().send_raw(
    "Page.navigate",
    serde_json::json!({ "url": "https://example.com" }),
).await?;
Ok(()) }
}

send_raw accepts anything Serialize for the params, so you can also pass your own request struct. If you find yourself reaching for send_raw on the same command repeatedly, implement CdpCommand for your own request struct and get the typed send method back. This is the same trait the curated domains use — there’s no second-class API.

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use thirtyfour::cdp::CdpCommand;

#[derive(Serialize)]
struct GetTitle;

#[derive(Deserialize)]
struct GetTitleResult { title: String }

impl CdpCommand for GetTitle {
    const METHOD: &'static str = "Page.getTitle";
    type Returns = GetTitleResult;
}
}

Resolving WebElements To CDP Handles

When you already have a WebElement and want to hand it to a CDP command (e.g. DOM.getBoxModel, Runtime.callFunctionOn), there are two helpers on WebElement:

#![allow(unused)]
fn main() {
use thirtyfour::prelude::*;
async fn run(driver: WebDriver) -> WebDriverResult<()> {
use thirtyfour::cdp::domains::dom::GetBoxModel;

let elem = driver.find(By::Css("button.submit")).await?;
let backend_id = elem.cdp_backend_node_id().await?;

let box_model = driver.cdp().send(GetBoxModel {
    backend_node_id: Some(backend_id),
    ..Default::default()
}).await?;
println!("box model: {:?}", box_model.model);
Ok(()) }
}

These are bridges between the WebDriver and CDP worlds — they let you use WebElement to find things and CDP to inspect them.

Where Next