Skip to content

Commit f806b66

Browse files
authored
fix: preserve query parameters in --cdp HTTP URLs (#982)
When --cdp is given an HTTP/HTTPS URL (e.g. http://host:5095?mode=Hello), resolve_cdp_url extracts host and port for CDP discovery but discards the query string. The discovered WebSocket URL therefore never includes the user's original query parameters, breaking relay servers that depend on them. Thread the original query string through discover_cdp_url and append it to the final WebSocket URL after host/port rewriting. WebSocket URLs (ws://, wss://) are already passed through unchanged and are unaffected. Fixes #977 Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
1 parent a7a59c9 commit f806b66

4 files changed

Lines changed: 112 additions & 14 deletions

File tree

cli/src/native/browser.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,12 +1324,13 @@ async fn resolve_cdp_url(input: &str) -> Result<String, String> {
13241324
.host_str()
13251325
.ok_or_else(|| format!("No host in CDP URL: {}", input))?;
13261326
let port = parsed.port().unwrap_or(9222);
1327-
return discover_cdp_url(host, port).await;
1327+
let query = parsed.query().map(|q| q.to_string());
1328+
return discover_cdp_url(host, port, query.as_deref()).await;
13281329
}
13291330

13301331
// Try as numeric port
13311332
if let Ok(port) = input.parse::<u16>() {
1332-
return discover_cdp_url("127.0.0.1", port).await;
1333+
return discover_cdp_url("127.0.0.1", port, None).await;
13331334
}
13341335

13351336
Err(format!(

cli/src/native/cdp/chrome.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
514514
for dir in &user_data_dirs {
515515
if let Some((port, ws_path)) = read_devtools_active_port(dir) {
516516
// Try HTTP endpoint first (pre-M144)
517-
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
517+
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port, None).await {
518518
return Ok(ws_url);
519519
}
520520
// M144+: direct WebSocket — verify the port is actually listening
@@ -533,7 +533,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
533533

534534
// Fallback: probe common ports
535535
for port in [9222u16, 9229] {
536-
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
536+
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port, None).await {
537537
return Ok(ws_url);
538538
}
539539
}

cli/src/native/cdp/discovery.rs

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,30 @@ const DEFAULT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(2);
1313
/// Tries three methods in order: `/json/version`, `/json/list`, and a direct
1414
/// WebSocket connection to `/devtools/browser`. The returned URL has its
1515
/// host/port rewritten to match the requested target.
16-
pub async fn discover_cdp_url(host: &str, port: u16) -> Result<String, String> {
17-
discover_cdp_url_with_timeout(host, port, DEFAULT_DISCOVERY_TIMEOUT).await
16+
///
17+
/// An optional `query` string (without the leading `?`) is appended to the
18+
/// final WebSocket URL so that user-supplied URL parameters (e.g.
19+
/// `?mode=Hello`) are forwarded to the remote endpoint.
20+
pub async fn discover_cdp_url(
21+
host: &str,
22+
port: u16,
23+
query: Option<&str>,
24+
) -> Result<String, String> {
25+
discover_cdp_url_with_timeout(host, port, query, DEFAULT_DISCOVERY_TIMEOUT).await
1826
}
1927

2028
/// Like [`discover_cdp_url`] but with a custom request timeout.
2129
pub async fn discover_cdp_url_with_timeout(
2230
host: &str,
2331
port: u16,
32+
query: Option<&str>,
2433
timeout: Duration,
2534
) -> Result<String, String> {
2635
// Primary: /json/version (standard path)
2736
let version_err = match fetch_cdp_info(host, port, timeout).await {
2837
Ok(info) => {
2938
if let Some(ws_url) = info.web_socket_debugger_url {
30-
return Ok(rewrite_ws_host(&ws_url, host, port));
39+
return Ok(append_query(&rewrite_ws_host(&ws_url, host, port), query));
3140
}
3241
format!(
3342
"No webSocketDebuggerUrl in /json/version at {}:{}",
@@ -39,15 +48,15 @@ pub async fn discover_cdp_url_with_timeout(
3948

4049
// Fallback: /json/list (returns target list; look for the browser target)
4150
let list_err = match fetch_cdp_list(host, port, timeout).await {
42-
Ok(ws_url) => return Ok(rewrite_ws_host(&ws_url, host, port)),
51+
Ok(ws_url) => return Ok(append_query(&rewrite_ws_host(&ws_url, host, port), query)),
4352
Err(e) => e,
4453
};
4554

4655
// Final fallback: direct WebSocket at /devtools/browser.
4756
// Chrome 136+ with UI-based remote debugging (chrome://inspect) exposes
4857
// CDP over WebSocket but does not serve HTTP discovery endpoints.
4958
match discover_cdp_ws(host, port, timeout).await {
50-
Ok(ws_url) => Ok(ws_url),
59+
Ok(ws_url) => Ok(append_query(&ws_url, query)),
5160
Err(ws_err) => Err(format!(
5261
"All CDP discovery methods failed for {}:{}: /json/version: {}; /json/list: {}; WebSocket: {}",
5362
host, port, version_err, list_err, ws_err
@@ -94,6 +103,29 @@ fn rewrite_ws_host(ws_url: &str, host: &str, port: u16) -> String {
94103
}
95104
}
96105

106+
/// Append a query string to a URL, preserving any existing query parameters.
107+
fn append_query(url: &str, query: Option<&str>) -> String {
108+
match query {
109+
Some(q) if !q.is_empty() => {
110+
if let Ok(mut parsed) = url::Url::parse(url) {
111+
{
112+
let mut pairs = parsed.query_pairs_mut();
113+
pairs.extend_pairs(url::form_urlencoded::parse(q.as_bytes()));
114+
}
115+
parsed.to_string()
116+
} else {
117+
// Fallback: raw string append
118+
if url.contains('?') {
119+
format!("{}&{}", url, q)
120+
} else {
121+
format!("{}?{}", url, q)
122+
}
123+
}
124+
}
125+
_ => url.to_string(),
126+
}
127+
}
128+
97129
/// Fetch `/json/list` and extract the `webSocketDebuggerUrl` from the first
98130
/// target with `type == "browser"`, or the first target if none has that type.
99131
async fn fetch_cdp_list(host: &str, port: u16, timeout: Duration) -> Result<String, String> {
@@ -210,7 +242,7 @@ mod tests {
210242
.await;
211243
});
212244

213-
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
245+
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
214246
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/", port));
215247
server.await.unwrap();
216248
}
@@ -224,7 +256,7 @@ mod tests {
224256
// /json/list and ws fallback both fail (server closes)
225257
});
226258

227-
let err = discover_cdp_url("127.0.0.1", port).await.unwrap_err();
259+
let err = discover_cdp_url("127.0.0.1", port, None).await.unwrap_err();
228260
assert!(err.contains("Invalid /json/version response"));
229261
server.await.unwrap();
230262
}
@@ -241,7 +273,7 @@ mod tests {
241273
).await;
242274
});
243275

244-
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
276+
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
245277
assert!(ws_url.contains("/devtools/browser/abc"));
246278
assert!(ws_url.contains(&port.to_string()));
247279
server.await.unwrap();
@@ -271,7 +303,7 @@ mod tests {
271303
let _ = ws.close(None).await;
272304
});
273305

274-
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
306+
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
275307
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/devtools/browser", port));
276308
server.await.unwrap();
277309
}
@@ -289,4 +321,67 @@ mod tests {
289321
let rewritten = rewrite_ws_host(original, "::1", 9222);
290322
assert_eq!(rewritten, "ws://[::1]:9222/devtools/browser/abc");
291323
}
324+
325+
#[test]
326+
fn append_query_adds_params_to_url_without_query() {
327+
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
328+
let result = append_query(url, Some("mode=Hello"));
329+
assert_eq!(
330+
result,
331+
"ws://127.0.0.1:9222/devtools/browser/abc?mode=Hello"
332+
);
333+
}
334+
335+
#[test]
336+
fn append_query_merges_with_existing_query() {
337+
let url = "ws://127.0.0.1:9222/devtools/browser/abc?token=xyz";
338+
let result = append_query(url, Some("mode=Hello"));
339+
assert_eq!(
340+
result,
341+
"ws://127.0.0.1:9222/devtools/browser/abc?token=xyz&mode=Hello"
342+
);
343+
}
344+
345+
#[test]
346+
fn append_query_noop_for_none() {
347+
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
348+
let result = append_query(url, None);
349+
assert_eq!(result, url);
350+
}
351+
352+
#[test]
353+
fn append_query_noop_for_empty() {
354+
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
355+
let result = append_query(url, Some(""));
356+
assert_eq!(result, url);
357+
}
358+
359+
#[test]
360+
fn append_query_handles_multiple_params() {
361+
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
362+
let result = append_query(url, Some("mode=Hello&token=abc"));
363+
assert_eq!(
364+
result,
365+
"ws://127.0.0.1:9222/devtools/browser/abc?mode=Hello&token=abc"
366+
);
367+
}
368+
369+
#[tokio::test]
370+
async fn discover_preserves_query_params() {
371+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
372+
let port = listener.local_addr().unwrap().port();
373+
let server = tokio::spawn(async move {
374+
accept_http(
375+
&listener,
376+
&http_200(r#"{"webSocketDebuggerUrl":"ws://127.0.0.1:1234/"}"#),
377+
)
378+
.await;
379+
});
380+
381+
let ws_url = discover_cdp_url("127.0.0.1", port, Some("mode=Hello"))
382+
.await
383+
.unwrap();
384+
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/?mode=Hello", port));
385+
server.await.unwrap();
386+
}
292387
}

cli/src/native/cdp/lightpanda.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,9 @@ async fn wait_for_lightpanda_ready(
257257
));
258258
}
259259

260-
match discover_cdp_url_with_timeout("127.0.0.1", port, LIGHTPANDA_DISCOVERY_TIMEOUT).await {
260+
match discover_cdp_url_with_timeout("127.0.0.1", port, None, LIGHTPANDA_DISCOVERY_TIMEOUT)
261+
.await
262+
{
261263
Ok(ws_url) => return Ok(ws_url),
262264
Err(err) => last_probe_error = Some(err),
263265
}

0 commit comments

Comments
 (0)