From d4b9004a6db5a34ea5414341478fc479dc2c1067 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 14 Mar 2026 15:46:08 -0500 Subject: [PATCH] fix: resolve snapshot hang over remote CDP (WSS) connections (#792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CDP WebSocket client had three issues causing snapshot to hang indefinitely when connected to remote browsers via WSS: 1. Binary WebSocket frames were silently dropped — remote CDP proxies (Browserless, Browserbase, etc.) may send large responses like Accessibility.getFullAXTree as Binary frames instead of Text frames. 2. Default tungstenite size limits (16 MiB frame / 64 MiB message) could be exceeded by large accessibility tree responses, causing the WebSocket connection to error out and the reader task to die. 3. When the reader task died, pending commands waited for the full 30-second timeout instead of failing immediately. Fixes #788 Co-authored-by: ctate <366502+ctate@users.noreply.github.com> --- cli/src/native/cdp/client.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/cli/src/native/cdp/client.rs b/cli/src/native/cdp/client.rs index e8056d9..6550c2c 100644 --- a/cli/src/native/cdp/client.rs +++ b/cli/src/native/cdp/client.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use futures_util::{SinkExt, StreamExt}; use serde_json::Value; use tokio::sync::{broadcast, oneshot, Mutex}; -use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; use tokio_tungstenite::tungstenite::Message; use super::types::{CdpCommand, CdpEvent, CdpMessage}; @@ -40,9 +40,19 @@ pub struct CdpClient { impl CdpClient { pub async fn connect(url: &str) -> Result { - let (ws_stream, _) = connect_async(url) - .await - .map_err(|e| format!("CDP WebSocket connect failed: {}", e))?; + // Use unlimited message/frame sizes to handle large CDP responses + // (e.g. Accessibility.getFullAXTree) over remote WSS connections where + // proxies may produce frames exceeding the default 16 MiB limit. + let ws_config = WebSocketConfig { + max_message_size: None, + max_frame_size: None, + ..Default::default() + }; + + let (ws_stream, _) = + tokio_tungstenite::connect_async_with_config(url, Some(ws_config), false) + .await + .map_err(|e| format!("CDP WebSocket connect failed: {}", e))?; let (ws_tx, mut ws_rx) = ws_stream.split(); let ws_tx = Arc::new(Mutex::new(ws_tx)); @@ -57,8 +67,14 @@ impl CdpClient { let reader_handle = tokio::spawn(async move { while let Some(msg) = ws_rx.next().await { + // Accept both Text and Binary frames — remote CDP proxies + // (e.g. Browserless) may send responses as Binary frames. let msg = match msg { Ok(Message::Text(text)) => text, + Ok(Message::Binary(data)) => match String::from_utf8(data) { + Ok(text) => text, + Err(_) => continue, + }, Ok(Message::Close(_)) => break, Ok(_) => continue, Err(_) => break, @@ -99,6 +115,11 @@ impl CdpClient { let _ = event_tx_clone.send(event); } } + + // Reader loop exited (connection closed or error). Drop all pending + // command senders so callers get an immediate channel-closed error + // instead of waiting for the 30-second timeout. + pending_clone.lock().await.clear(); }); Ok(Self {