From 73e4ae1c1b56520edf248904e87f0f1a72097ad4 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Fri, 13 Feb 2026 22:40:52 +0100 Subject: [PATCH] feat: support custom headers for CDP connect (#396) - Pass headers from CLI --headers flag through to daemon via AGENT_BROWSER_HEADERS env var - Apply headers post-connection using context.setExtraHTTPHeaders() in connectViaCDP() - Add AGENT_BROWSER_HEADERS env var fallback in flags.rs - Parse AGENT_BROWSER_HEADERS in daemon.ts auto-launch flow Playwright's connectOverCDP() does not support connection-time headers, so headers are applied to browser contexts after the CDP connection is established. --- cli/src/connection.rs | 9 +++++++++ cli/src/flags.rs | 2 +- cli/src/main.rs | 1 + src/browser.ts | 12 ++++++++++-- src/daemon.ts | 12 ++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cli/src/connection.rs b/cli/src/connection.rs index 220d5224..dde67229 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -220,6 +220,7 @@ pub fn ensure_daemon( provider: Option<&str>, device: Option<&str>, session_name: Option<&str>, + headers: Option<&str>, ) -> Result { // Check if daemon is running AND responsive if is_daemon_running(session) && daemon_ready(session) { @@ -364,6 +365,10 @@ pub fn ensure_daemon( cmd.env("AGENT_BROWSER_SESSION_NAME", sn); } + if let Some(h) = headers { + cmd.env("AGENT_BROWSER_HEADERS", h); + } + // Create new process group and session to fully detach unsafe { cmd.pre_exec(|| { @@ -447,6 +452,10 @@ pub fn ensure_daemon( cmd.env("AGENT_BROWSER_SESSION_NAME", sn); } + if let Some(h) = headers { + cmd.env("AGENT_BROWSER_HEADERS", h); + } + // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; const DETACHED_PROCESS: u32 = 0x00000008; diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 84d41ebf..a8f5d3fc 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -53,7 +53,7 @@ pub fn parse_flags(args: &[String]) -> Flags { headed: false, debug: false, session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()), - headers: None, + headers: env::var("AGENT_BROWSER_HEADERS").ok(), executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(), cdp: None, extensions: extensions_env, diff --git a/cli/src/main.rs b/cli/src/main.rs index 7be0f153..523f9034 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -226,6 +226,7 @@ fn main() { flags.provider.as_deref(), flags.device.as_deref(), flags.session_name.as_deref(), + flags.headers.as_deref(), ) { Ok(result) => result, Err(e) => { diff --git a/src/browser.ts b/src/browser.ts index d8c7298d..91cc1044 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1117,7 +1117,7 @@ export class BrowserManager { } if (cdpEndpoint) { - await this.connectViaCDP(cdpEndpoint); + await this.connectViaCDP(cdpEndpoint, options.headers); return; } @@ -1310,7 +1310,7 @@ export class BrowserManager { * Connect to a running browser via CDP (Chrome DevTools Protocol) * @param cdpEndpoint Either a port number (as string) or a full WebSocket URL (ws:// or wss://) */ - private async connectViaCDP(cdpEndpoint: string | undefined): Promise { + private async connectViaCDP(cdpEndpoint: string | undefined, headers?: Record): Promise { if (!cdpEndpoint) { throw new Error('CDP endpoint is required for CDP connection'); } @@ -1374,6 +1374,14 @@ export class BrowserManager { } this.activePageIndex = 0; + + // Apply custom headers post-connection (Playwright's connectOverCDP doesn't support + // connection-time headers, so we set them on the context after connecting) + if (headers && Object.keys(headers).length > 0) { + for (const context of contexts) { + await context.setExtraHTTPHeaders(headers); + } + } } catch (error) { // Clean up browser connection if validation or setup failed await browser.close().catch(() => {}); diff --git a/src/daemon.ts b/src/daemon.ts index 36d5acc4..7290da45 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -417,6 +417,17 @@ export async function startDaemon(options?: { const ignoreHTTPSErrors = process.env.AGENT_BROWSER_IGNORE_HTTPS_ERRORS === '1'; const allowFileAccess = process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1'; + + // Parse custom headers from env (JSON string) + let headers: Record | undefined; + if (process.env.AGENT_BROWSER_HEADERS) { + try { + headers = JSON.parse(process.env.AGENT_BROWSER_HEADERS); + } catch { + /* ignore invalid JSON */ + } + } + await manager.launch({ id: 'auto', action: 'launch' as const, @@ -430,6 +441,7 @@ export async function startDaemon(options?: { proxy, ignoreHTTPSErrors: ignoreHTTPSErrors, allowFileAccess: allowFileAccess, + headers, autoStateFilePath: getSessionAutoStatePath(), }); }