@@ -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.
2129pub 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.
99131async 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}
0 commit comments