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.
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
| Method | Description |
|---|---|
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
- First build is slow: V8 compiles from source (~5 min). Incremental builds are seconds.
- cmake required: Stealth feature needs cmake for BoringSSL.
- Pin a Git tag: For reproducible builds.
- Not on crates.io: Must use Git dependency.
panic = "unwind": Must stay in release profile (already set in workspace).
CDP vs Embedded
| Aspect | CDP | Embedded |
|---|---|---|
| Language | Any (HTTP/WS) | Rust only |
| Isolation | Process-level | Same process |
| Performance | Serialization + network | Zero overhead |
| Deployment | Separate process | Embedded |
| Debugging | Remote DevTools | In-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.