Skip to content

Commit bfd1533

Browse files
authored
fix: stabilize /browser and /diff (#522)
* fix: stabilize /browser and /diff Fixes #508 Fixes #473 * test(browser): add proxy-env regression for CDP discovery
1 parent 773ea6d commit bfd1533

2 files changed

Lines changed: 180 additions & 0 deletions

File tree

code-rs/browser/src/manager.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
25042516
mod 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\nContent-Type: application/json\r\nContent-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={:?}\nstdout:\n{}\nstderr:\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
}

code-rs/tui/src/history_cell/diff.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ pub(crate) fn diff_record_from_string(title: String, diff: &str) -> DiffRecord {
113113
(DiffLineKind::Addition, rest.to_string())
114114
} else if let Some(rest) = line.strip_prefix('-') {
115115
(DiffLineKind::Removal, rest.to_string())
116+
} else if let Some(rest) = line.strip_prefix(' ') {
117+
(DiffLineKind::Context, rest.to_string())
116118
} else {
117119
(DiffLineKind::Context, line)
118120
};
@@ -159,4 +161,21 @@ mod tests {
159161
assert!(!lines[0].content.contains('\u{001B}'));
160162
assert!(!lines[1].content.contains('\u{001B}'));
161163
}
164+
165+
#[test]
166+
fn diff_record_strips_context_prefix_space() {
167+
let diff = concat!(
168+
"@@ -1,3 +1,3 @@\n",
169+
" unchanged\n",
170+
"-old\n",
171+
"+new\n",
172+
);
173+
let record = diff_record_from_string(String::new(), diff);
174+
assert_eq!(record.hunks.len(), 1);
175+
let lines = &record.hunks[0].lines;
176+
assert_eq!(lines.len(), 3);
177+
assert_eq!(lines[0].content, "unchanged");
178+
assert_eq!(lines[1].content, "old");
179+
assert_eq!(lines[2].content, "new");
180+
}
162181
}

0 commit comments

Comments
 (0)