Obscura Rust Library — Browser/Page/Element Complete API

Embed Obscura as a Rust library in your project. Browser/Page/Element/CookieStore API, request interception, event callbacks, and build notes.

16Yun Engineering TeamJul 8, 20262 min read

Why the Rust Library

CDP mode communicates over WebSocket and works with any language. For Rust projects, calling the engine in-process is significantly faster:

  • Zero serialization overhead
  • Zero network latency
  • Direct memory access to the V8 Isolate

The obscura crate provides the complete embedded API.

Add Dependency

Obscura builds V8 from source, so it is not on crates.io. Use a Git dependency:

[dependencies]
obscura = { git = "https://example.16yun.cn/obscura" }
tokio = { version = "1", features = ["rt", "macros"] }
anyhow = "1"

(The git URL above is a placeholder; replace it with the project repository URL.)

Pin a tag for reproducible builds:

obscura = { git = "https://example.16yun.cn/obscura", tag = "v0.1.7" }

The first build compiles V8 from source (~5 minutes). Incremental builds are seconds.

Browser

use obscura::Browser;
 
let browser = Browser::new()?;
 
let browser = Browser::builder()
    .stealth(true)
    .proxy("http://user:pass@proxy.16yun.cn:8888")
    .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...")
    .storage_dir("/data/obscura")
    .build()?;

Page

use std::time::Duration;
 
let mut page = browser.new_page().await?;
page.goto("https://example.com").await?;
 
println!("URL: {}", page.url());
println!("HTML bytes: {}", page.content().len());
 
let title = page.evaluate("document.title");
println!("Title: {}", title);
 
let el = page.wait_for_selector("h1", Duration::from_secs(5)).await?;
if let Some(element) = el {
    println!("Heading: {}", element.text());
}

Page API

MethodDescription
goto(url)Navigate and wait for load
content()Rendered HTML
url()Current URL
evaluate(js)Run JS, returns serde_json::Value
query_selector(css)First matching Element
wait_for_selector(css, Duration)Poll until element appears
settle(max_ms)Drive event loop
on_request(cb)Request callback
on_response(cb)Response callback
enable_interception()Interception channel
add_preload_script(js)Pre-navigation script

Element

if let Some(el) = page.query_selector("a.link").await? {
    println!("Text: {}", el.text());
    println!("Href: {:?}", el.attribute("href"));
    el.click();
}

CookieStore

let all = page.context.cookie_jar.get_all_cookies();
 
let url = url::Url::parse("https://example.com")?;
let cookies = page.context.cookie_jar.get_for_url(&url);
 
page.context.cookie_jar.set(Cookie::new("session", "abc123"));
 
page.context.cookie_jar.save_to_file("/data/cookies.json")?;

Request Interception

let mut rx = page.enable_interception();
 
tokio::spawn(async move {
    while let Some(req) = rx.recv().await {
        let action = if req.url.contains("/track") {
            InterceptResolution::Fail { reason: "blocked".into() }
        } else if req.url.ends_with("/api/data") {
            InterceptResolution::Fulfill {
                status: 200,
                headers: Default::default(),
                body: r#"{"data": "mocked"}"#.into(),
            }
        } else {
            InterceptResolution::Continue {
                url: None, method: None, headers: None, body: None,
            }
        };
        let _ = req.resolver.send(action);
    }
});
 
page.goto("https://example.com").await?;
page.settle(2000).await;

Preload Script

page.add_preload_script("window.__obscura = true;");
page.goto("https://example.com").await?;

Passive Callbacks

page.on_request(Arc::new(|info| {
    tracing::info!("→ {} {}", info.method, info.url);
}));
 
page.on_response(Arc::new(|info, resp| {
    if info.resource_type == ResourceType::Fetch {
        tracing::info!("← API: {} ({} bytes)", info.url, resp.body.len());
    }
}));

Complete Example: Scraping Loop

use obscura::{Browser, InterceptResolution, ResourceType};
use std::sync::Arc;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let browser = Browser::builder()
        .stealth(true)
        .proxy("http://user:pass@proxy.16yun.cn:8888")
        .build()?;
 
    let mut page = browser.new_page().await?;
 
    let intercepted = Arc::new(tokio::sync::Mutex::new(Vec::new()));
    let intercepted_clone = intercepted.clone();
 
    page.on_response(Arc::new(move |info, resp| {
        if info.resource_type == ResourceType::Fetch {
            let mut data = intercepted_clone.blocking_lock();
            data.push((info.url.clone(), resp.body.clone()));
        }
    }));
 
    page.goto("https://example.com").await?;
    page.settle(2000).await;
 
    if let Some(h1) = page.wait_for_selector("h1", Duration::from_secs(5)).await? {
        println!("Title: {}", h1.text());
    }
 
    let data = intercepted.lock().await;
    for (url, body) in data.iter() {
        println!("API: {} ({} bytes)", url, body.len());
    }
 
    Ok(())
}

Build Notes

  1. First build is slow: V8 compiles from source (~5 min). Incremental builds are seconds.
  2. cmake required: Stealth feature needs cmake for BoringSSL.
  3. Pin a Git tag: For reproducible builds.
  4. Not on crates.io: Must use Git dependency.
  5. panic = "unwind": Must stay in release profile (already set in workspace).

CDP vs Embedded

AspectCDPEmbedded
LanguageAny (HTTP/WS)Rust only
IsolationProcess-levelSame process
PerformanceSerialization + networkZero overhead
DeploymentSeparate processEmbedded
DebuggingRemote DevToolsIn-process logs

Summary

The obscura crate provides a complete Rust embedded browser API. For Rust projects, embedding Obscura avoids CDP serialization and network overhead while retaining full browser capabilities — page rendering, JS execution, request interception, cookie management, and Stealth anti-detection.

Need an enterprise proxy plan?

We can tailor architecture to your target domains, concurrency, and reliability goals.