fix(serve): plumb --user-agent through CDP server (#71) (#76)

obscura serve --user-agent <ua> only logged the value via tracing::info!
and never passed it to the CDP server, so every navigation kept using
the default Chrome UA string and httpbin.org/user-agent reported the
default rather than the override.

Plumb the option end-to-end:

- BrowserContext::with_full_options(id, proxy, stealth, user_agent)
  initializes both BrowserContext::user_agent and the underlying
  ObscuraHttpClient::user_agent RwLock at construction time so the
  first navigation already sees the override.
- CdpContext::new_with_full_options forwards the UA to the default
  BrowserContext.
- obscura_cdp::start_with_full_options accepts the UA and passes it
  through cdp_processor.
- main.rs Serve arm calls start_with_full_options with the parsed UA.
- run_multi_worker_serve forwards --user-agent <ua> when spawning
  worker subprocesses so multi-worker mode honors the flag too.

Existing two-arg start_with_options and three-arg with_options stay
as thin shims (call the full variant with None) so older callers and
the public crate API remain source-compatible.

Three new unit tests in obscura-browser cover the propagation, the
Chrome default fallback, and the legacy with_options path.

Generated by Ora Studio
Vibe coded by ousamabenyounes

Co-authored-by: Ora Studio <[email protected]>
This commit is contained in:
Ben YounesandOra Studio authored and GitHub committed 2026-05-03 17:32:38 +02:00
1 parent 9d8d3a02a9
commit 536072b323
5 files changed
+86 -7

No files matched your search

+57 -1
View File
@@ -30,6 +30,15 @@ impl BrowserContext {
}
pub fn with_options(id: String, proxy_url: Option<String>, stealth: bool) -> Self {
Self::with_full_options(id, proxy_url, stealth, None)
}
pub fn with_full_options(
id: String,
proxy_url: Option<String>,
stealth: bool,
user_agent: Option<String>,
) -> Self {
let cookie_jar = Arc::new(CookieJar::new());
let mut client = ObscuraHttpClient::with_options(
cookie_jar.clone(),
@@ -38,12 +47,21 @@ impl BrowserContext {
if stealth {
client.block_trackers = true;
}
let resolved_ua = user_agent.unwrap_or_else(|| {
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36".to_string()
});
// Sync the http client's UA at construction so navigation requests pick it
// up before any async setup runs. The lock has no other holders here, so
// try_write always succeeds; we fall back silently if it ever fails.
if let Ok(mut guard) = client.user_agent.try_write() {
*guard = resolved_ua.clone();
}
let http_client = Arc::new(client);
BrowserContext {
id,
cookie_jar,
http_client,
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36".to_string(),
user_agent: resolved_ua,
proxy_url,
robots_cache: Arc::new(RobotsCache::new()),
obey_robots: false,
@@ -55,3 +73,41 @@ impl BrowserContext {
Self::with_options(id, proxy_url, false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn with_full_options_propagates_user_agent_to_http_client() {
let ctx = BrowserContext::with_full_options(
"test".to_string(),
None,
false,
Some("Custom-UA/1.0".to_string()),
);
assert_eq!(ctx.user_agent, "Custom-UA/1.0");
let client_ua = ctx.http_client.user_agent.read().await.clone();
assert_eq!(client_ua, "Custom-UA/1.0");
}
#[tokio::test(flavor = "current_thread")]
async fn with_full_options_falls_back_to_chrome_default() {
let ctx = BrowserContext::with_full_options(
"test".to_string(),
None,
false,
None,
);
assert!(ctx.user_agent.contains("Chrome"));
let client_ua = ctx.http_client.user_agent.read().await.clone();
assert!(client_ua.contains("Chrome"));
assert_eq!(ctx.user_agent, client_ua);
}
#[tokio::test(flavor = "current_thread")]
async fn with_options_keeps_default_user_agent() {
let ctx = BrowserContext::with_options("test".to_string(), None, false);
assert!(ctx.user_agent.contains("Chrome"));
}
}