@@ -141,6 +141,16 @@ fn should_stop_handler<E: std::fmt::Display>(
141141 false
142142 }
143143 Err ( err) => {
144+ let message = err. to_string ( ) ;
145+ let message_lower = message. to_ascii_lowercase ( ) ;
146+ // Chromiumoxide surfaces "oneshot canceled" when a request future is dropped
147+ // (e.g., due to our own timeouts). These are expected and should not
148+ // be treated as connection failures.
149+ if message_lower. contains ( "oneshot canceled" ) || message_lower. contains ( "oneshot cancelled" ) {
150+ * consecutive_errors = 0 ;
151+ debug ! ( "{label} event handler error ignored: {message}" ) ;
152+ return false ;
153+ }
144154 * consecutive_errors = consecutive_errors. saturating_add ( 1 ) ;
145155 let count = * consecutive_errors;
146156 if count <= HANDLER_ERROR_LIMIT {
@@ -161,6 +171,7 @@ async fn discover_ws_via_host_port(host: &str, port: u16) -> Result<String> {
161171
162172 let client_start = tokio:: time:: Instant :: now ( ) ;
163173 let client = Client :: builder ( )
174+ . no_proxy ( )
164175 . timeout ( Duration :: from_secs ( 5 ) ) // Allow Chrome time to bring up /json/version on fresh launch
165176 . build ( )
166177 . map_err ( |e| BrowserError :: CdpError ( format ! ( "Failed to build HTTP client: {}" , e) ) ) ?;
@@ -248,6 +259,7 @@ async fn scan_for_chrome_debug_port() -> Option<u16> {
248259 let test_future = async move {
249260 let url = format ! ( "http://127.0.0.1:{}/json/version" , port) ;
250261 let client = Client :: builder ( )
262+ . no_proxy ( )
251263 . timeout ( Duration :: from_millis ( 200 ) ) // Shorter timeout for parallel tests
252264 . build ( )
253265 . ok ( ) ?;
@@ -2502,7 +2514,28 @@ pub struct BrowserStatus {
25022514
25032515#[ cfg( test) ]
25042516mod tests {
2517+ use super :: discover_ws_via_host_port;
25052518 use super :: should_restart_handler;
2519+ use super :: should_stop_handler;
2520+ use std:: io:: Read ;
2521+ use std:: io:: Write ;
2522+ use std:: net:: TcpListener ;
2523+ use std:: process:: Command ;
2524+ use std:: sync:: Arc ;
2525+ use std:: sync:: atomic:: AtomicBool ;
2526+ use std:: sync:: atomic:: Ordering ;
2527+ use std:: thread;
2528+ use std:: time:: Duration ;
2529+ use std:: time:: Instant ;
2530+
2531+ #[ derive( Debug ) ]
2532+ struct TestError ( & ' static str ) ;
2533+
2534+ impl std:: fmt:: Display for TestError {
2535+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
2536+ f. write_str ( self . 0 )
2537+ }
2538+ }
25062539
25072540 #[ test]
25082541 fn handler_restarts_after_repeated_errors ( ) {
@@ -2511,4 +2544,132 @@ mod tests {
25112544 assert ! ( !should_restart_handler( 2 ) ) ;
25122545 assert ! ( should_restart_handler( 3 ) ) ;
25132546 }
2547+
2548+ #[ test]
2549+ fn handler_ignores_oneshot_cancellations ( ) {
2550+ let mut consecutive_errors = 0u32 ;
2551+ for _ in 0 ..10 {
2552+ let should_stop = should_stop_handler (
2553+ "[test]" ,
2554+ Err ( TestError ( "oneshot canceled" ) ) ,
2555+ & mut consecutive_errors,
2556+ ) ;
2557+ assert ! ( !should_stop) ;
2558+ assert_eq ! ( consecutive_errors, 0 ) ;
2559+ }
2560+ }
2561+
2562+ const TEST_PROXY_WS_URL : & str = "ws://proxy.invalid/devtools/browser/proxy" ;
2563+ const TEST_TARGET_WS_URL : & str = "ws://target.invalid/devtools/browser/target" ;
2564+
2565+ fn spawn_json_version_server (
2566+ ws_url : & str ,
2567+ ) -> ( u16 , Arc < AtomicBool > , thread:: JoinHandle < ( ) > ) {
2568+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . expect ( "bind server" ) ;
2569+ listener
2570+ . set_nonblocking ( true )
2571+ . expect ( "set non-blocking" ) ;
2572+ let port = listener. local_addr ( ) . expect ( "server addr" ) . port ( ) ;
2573+ let stop = Arc :: new ( AtomicBool :: new ( false ) ) ;
2574+ let stop_thread = Arc :: clone ( & stop) ;
2575+ let ws_url = ws_url. to_string ( ) ;
2576+
2577+ let handle = thread:: spawn ( move || {
2578+ let deadline = Instant :: now ( ) + Duration :: from_secs ( 10 ) ;
2579+ while !stop_thread. load ( Ordering :: Relaxed ) && Instant :: now ( ) < deadline {
2580+ match listener. accept ( ) {
2581+ Ok ( ( mut stream, _) ) => {
2582+ let mut buffer = [ 0u8 ; 1024 ] ;
2583+ let _ = stream. read ( & mut buffer) ;
2584+
2585+ let body = format ! ( r#"{{"webSocketDebuggerUrl":"{ws_url}"}}"# ) ;
2586+ let response = format ! (
2587+ "HTTP/1.1 200 OK\r \n Content-Type: application/json\r \n Content-Length: {}\r \n \r \n {}" ,
2588+ body. len( ) ,
2589+ body
2590+ ) ;
2591+ let _ = stream. write_all ( response. as_bytes ( ) ) ;
2592+ }
2593+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: WouldBlock => {
2594+ thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
2595+ }
2596+ Err ( _) => break ,
2597+ }
2598+ }
2599+ } ) ;
2600+
2601+ ( port, stop, handle)
2602+ }
2603+
2604+ #[ test]
2605+ fn cdp_discovery_ignores_proxy_env_vars ( ) {
2606+ let ( proxy_port, proxy_stop, proxy_handle) =
2607+ spawn_json_version_server ( TEST_PROXY_WS_URL ) ;
2608+ let ( target_port, target_stop, target_handle) =
2609+ spawn_json_version_server ( TEST_TARGET_WS_URL ) ;
2610+
2611+ let exe = std:: env:: current_exe ( ) . expect ( "current exe" ) ;
2612+ let proxy_url = format ! ( "http://127.0.0.1:{proxy_port}" ) ;
2613+
2614+ let output = Command :: new ( exe)
2615+ . arg ( "--exact" )
2616+ . arg ( "manager::tests::cdp_discovery_ignores_proxy_env_vars_child" )
2617+ . arg ( "--ignored" )
2618+ . arg ( "--nocapture" )
2619+ . env ( "CODE_BROWSER_TEST_TARGET_PORT" , target_port. to_string ( ) )
2620+ . env ( "HTTP_PROXY" , & proxy_url)
2621+ . env ( "HTTPS_PROXY" , & proxy_url)
2622+ . env ( "ALL_PROXY" , & proxy_url)
2623+ . env ( "http_proxy" , & proxy_url)
2624+ . env ( "https_proxy" , & proxy_url)
2625+ . env ( "all_proxy" , & proxy_url)
2626+ . env ( "NO_PROXY" , "" )
2627+ . env ( "no_proxy" , "" )
2628+ . output ( )
2629+ . expect ( "spawn child test" ) ;
2630+
2631+ proxy_stop. store ( true , Ordering :: Relaxed ) ;
2632+ target_stop. store ( true , Ordering :: Relaxed ) ;
2633+ let _ = proxy_handle. join ( ) ;
2634+ let _ = target_handle. join ( ) ;
2635+
2636+ if !output. status . success ( ) {
2637+ panic ! (
2638+ "child test failed: status={:?}\n stdout:\n {}\n stderr:\n {}" ,
2639+ output. status,
2640+ String :: from_utf8_lossy( & output. stdout) ,
2641+ String :: from_utf8_lossy( & output. stderr) ,
2642+ ) ;
2643+ }
2644+ }
2645+
2646+ #[ ignore]
2647+ #[ tokio:: test]
2648+ async fn cdp_discovery_ignores_proxy_env_vars_child ( ) {
2649+ let target_port: u16 = std:: env:: var ( "CODE_BROWSER_TEST_TARGET_PORT" )
2650+ . expect ( "CODE_BROWSER_TEST_TARGET_PORT" )
2651+ . parse ( )
2652+ . expect ( "valid port" ) ;
2653+
2654+ let url = format ! ( "http://127.0.0.1:{target_port}/json/version" ) ;
2655+ let default_client = reqwest:: Client :: builder ( )
2656+ . timeout ( Duration :: from_secs ( 2 ) )
2657+ . build ( )
2658+ . expect ( "build default client" ) ;
2659+ let resp = default_client
2660+ . get ( & url)
2661+ . send ( )
2662+ . await
2663+ . expect ( "default request" ) ;
2664+
2665+ let proxy_version: super :: JsonVersion =
2666+ resp. json ( ) . await . expect ( "parse proxy json" ) ;
2667+ assert_eq ! ( proxy_version. web_socket_debugger_url, TEST_PROXY_WS_URL ) ;
2668+
2669+ let discovered = discover_ws_via_host_port ( "127.0.0.1" , target_port)
2670+ . await
2671+ . expect ( "discover ws url" ) ;
2672+ assert_eq ! ( discovered, TEST_TARGET_WS_URL ) ;
2673+ }
2674+
25142675}
0 commit comments