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>
This commit is contained in:
Chris Tate
2026-03-23 18:47:23 -05:00
committed by GitHub
co-authored by ctate
parent a7a59c94f3
commit f806b666ba
4 changed files with 112 additions and 14 deletions
+3 -2
View File
@@ -1324,12 +1324,13 @@ async fn resolve_cdp_url(input: &str) -> Result<String, String> {
.host_str()
.ok_or_else(|| format!("No host in CDP URL: {}", input))?;
let port = parsed.port().unwrap_or(9222);
return discover_cdp_url(host, port).await;
let query = parsed.query().map(|q| q.to_string());
return discover_cdp_url(host, port, query.as_deref()).await;
}
// Try as numeric port
if let Ok(port) = input.parse::<u16>() {
return discover_cdp_url("127.0.0.1", port).await;
return discover_cdp_url("127.0.0.1", port, None).await;
}
Err(format!(
+2 -2
View File
@@ -514,7 +514,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
for dir in &user_data_dirs {
if let Some((port, ws_path)) = read_devtools_active_port(dir) {
// Try HTTP endpoint first (pre-M144)
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port, None).await {
return Ok(ws_url);
}
// M144+: direct WebSocket — verify the port is actually listening
@@ -533,7 +533,7 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
// Fallback: probe common ports
for port in [9222u16, 9229] {
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port).await {
if let Ok(ws_url) = discover_cdp_url("127.0.0.1", port, None).await {
return Ok(ws_url);
}
}
+104 -9
View File
@@ -13,21 +13,30 @@ const DEFAULT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(2);
/// Tries three methods in order: `/json/version`, `/json/list`, and a direct
/// WebSocket connection to `/devtools/browser`. The returned URL has its
/// host/port rewritten to match the requested target.
pub async fn discover_cdp_url(host: &str, port: u16) -> Result<String, String> {
discover_cdp_url_with_timeout(host, port, DEFAULT_DISCOVERY_TIMEOUT).await
///
/// An optional `query` string (without the leading `?`) is appended to the
/// final WebSocket URL so that user-supplied URL parameters (e.g.
/// `?mode=Hello`) are forwarded to the remote endpoint.
pub async fn discover_cdp_url(
host: &str,
port: u16,
query: Option<&str>,
) -> Result<String, String> {
discover_cdp_url_with_timeout(host, port, query, DEFAULT_DISCOVERY_TIMEOUT).await
}
/// Like [`discover_cdp_url`] but with a custom request timeout.
pub async fn discover_cdp_url_with_timeout(
host: &str,
port: u16,
query: Option<&str>,
timeout: Duration,
) -> Result<String, String> {
// Primary: /json/version (standard path)
let version_err = match fetch_cdp_info(host, port, timeout).await {
Ok(info) => {
if let Some(ws_url) = info.web_socket_debugger_url {
return Ok(rewrite_ws_host(&ws_url, host, port));
return Ok(append_query(&rewrite_ws_host(&ws_url, host, port), query));
}
format!(
"No webSocketDebuggerUrl in /json/version at {}:{}",
@@ -39,7 +48,7 @@ pub async fn discover_cdp_url_with_timeout(
// Fallback: /json/list (returns target list; look for the browser target)
let list_err = match fetch_cdp_list(host, port, timeout).await {
Ok(ws_url) => return Ok(rewrite_ws_host(&ws_url, host, port)),
Ok(ws_url) => return Ok(append_query(&rewrite_ws_host(&ws_url, host, port), query)),
Err(e) => e,
};
@@ -47,7 +56,7 @@ pub async fn discover_cdp_url_with_timeout(
// Chrome 136+ with UI-based remote debugging (chrome://inspect) exposes
// CDP over WebSocket but does not serve HTTP discovery endpoints.
match discover_cdp_ws(host, port, timeout).await {
Ok(ws_url) => Ok(ws_url),
Ok(ws_url) => Ok(append_query(&ws_url, query)),
Err(ws_err) => Err(format!(
"All CDP discovery methods failed for {}:{}: /json/version: {}; /json/list: {}; WebSocket: {}",
host, port, version_err, list_err, ws_err
@@ -94,6 +103,29 @@ fn rewrite_ws_host(ws_url: &str, host: &str, port: u16) -> String {
}
}
/// Append a query string to a URL, preserving any existing query parameters.
fn append_query(url: &str, query: Option<&str>) -> String {
match query {
Some(q) if !q.is_empty() => {
if let Ok(mut parsed) = url::Url::parse(url) {
{
let mut pairs = parsed.query_pairs_mut();
pairs.extend_pairs(url::form_urlencoded::parse(q.as_bytes()));
}
parsed.to_string()
} else {
// Fallback: raw string append
if url.contains('?') {
format!("{}&{}", url, q)
} else {
format!("{}?{}", url, q)
}
}
}
_ => url.to_string(),
}
}
/// Fetch `/json/list` and extract the `webSocketDebuggerUrl` from the first
/// target with `type == "browser"`, or the first target if none has that type.
async fn fetch_cdp_list(host: &str, port: u16, timeout: Duration) -> Result<String, String> {
@@ -210,7 +242,7 @@ mod tests {
.await;
});
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/", port));
server.await.unwrap();
}
@@ -224,7 +256,7 @@ mod tests {
// /json/list and ws fallback both fail (server closes)
});
let err = discover_cdp_url("127.0.0.1", port).await.unwrap_err();
let err = discover_cdp_url("127.0.0.1", port, None).await.unwrap_err();
assert!(err.contains("Invalid /json/version response"));
server.await.unwrap();
}
@@ -241,7 +273,7 @@ mod tests {
).await;
});
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
assert!(ws_url.contains("/devtools/browser/abc"));
assert!(ws_url.contains(&port.to_string()));
server.await.unwrap();
@@ -271,7 +303,7 @@ mod tests {
let _ = ws.close(None).await;
});
let ws_url = discover_cdp_url("127.0.0.1", port).await.unwrap();
let ws_url = discover_cdp_url("127.0.0.1", port, None).await.unwrap();
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/devtools/browser", port));
server.await.unwrap();
}
@@ -289,4 +321,67 @@ mod tests {
let rewritten = rewrite_ws_host(original, "::1", 9222);
assert_eq!(rewritten, "ws://[::1]:9222/devtools/browser/abc");
}
#[test]
fn append_query_adds_params_to_url_without_query() {
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
let result = append_query(url, Some("mode=Hello"));
assert_eq!(
result,
"ws://127.0.0.1:9222/devtools/browser/abc?mode=Hello"
);
}
#[test]
fn append_query_merges_with_existing_query() {
let url = "ws://127.0.0.1:9222/devtools/browser/abc?token=xyz";
let result = append_query(url, Some("mode=Hello"));
assert_eq!(
result,
"ws://127.0.0.1:9222/devtools/browser/abc?token=xyz&mode=Hello"
);
}
#[test]
fn append_query_noop_for_none() {
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
let result = append_query(url, None);
assert_eq!(result, url);
}
#[test]
fn append_query_noop_for_empty() {
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
let result = append_query(url, Some(""));
assert_eq!(result, url);
}
#[test]
fn append_query_handles_multiple_params() {
let url = "ws://127.0.0.1:9222/devtools/browser/abc";
let result = append_query(url, Some("mode=Hello&token=abc"));
assert_eq!(
result,
"ws://127.0.0.1:9222/devtools/browser/abc?mode=Hello&token=abc"
);
}
#[tokio::test]
async fn discover_preserves_query_params() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let server = tokio::spawn(async move {
accept_http(
&listener,
&http_200(r#"{"webSocketDebuggerUrl":"ws://127.0.0.1:1234/"}"#),
)
.await;
});
let ws_url = discover_cdp_url("127.0.0.1", port, Some("mode=Hello"))
.await
.unwrap();
assert_eq!(ws_url, format!("ws://127.0.0.1:{}/?mode=Hello", port));
server.await.unwrap();
}
}
+3 -1
View File
@@ -257,7 +257,9 @@ async fn wait_for_lightpanda_ready(
));
}
match discover_cdp_url_with_timeout("127.0.0.1", port, LIGHTPANDA_DISCOVERY_TIMEOUT).await {
match discover_cdp_url_with_timeout("127.0.0.1", port, None, LIGHTPANDA_DISCOVERY_TIMEOUT)
.await
{
Ok(ws_url) => return Ok(ws_url),
Err(err) => last_probe_error = Some(err),
}