diff --git a/crates/common/src/config/mux.rs b/crates/common/src/config/mux.rs index 8602cba4..419a097b 100644 --- a/crates/common/src/config/mux.rs +++ b/crates/common/src/config/mux.rs @@ -369,7 +369,7 @@ async fn fetch_lido_registry_keys( } async fn fetch_ssv_pubkeys( - api_url: Url, + mut api_url: Url, chain: Chain, node_operator_id: U256, http_timeout: Duration, @@ -386,6 +386,15 @@ async fn fetch_ssv_pubkeys( let mut pubkeys: Vec = vec![]; let mut page = 1; + // Validate the URL - this appends a trailing slash if missing as efficiently as + // possible + if !api_url.path().ends_with('/') { + match api_url.path_segments_mut() { + Ok(mut segments) => segments.push(""), // Analogous to a trailing slash + Err(_) => bail!("SSV API URL is not a valid base URL"), + }; + } + loop { let route = format!( "{chain_name}/validators/in_operator/{node_operator_id}?perPage={MAX_PER_PAGE}&page={page}", diff --git a/crates/common/src/config/pbs.rs b/crates/common/src/config/pbs.rs index c2c30d2f..7bcf91e3 100644 --- a/crates/common/src/config/pbs.rs +++ b/crates/common/src/config/pbs.rs @@ -404,5 +404,5 @@ pub async fn load_pbs_custom_config() -> Result<(PbsModuleC /// Default URL for the SSV network API fn default_ssv_api_url() -> Url { - Url::parse("https://api.ssv.network/api/v4").expect("default URL is valid") + Url::parse("https://api.ssv.network/api/v4/").expect("default URL is valid") } diff --git a/tests/src/mock_ssv.rs b/tests/src/mock_ssv.rs index 3fa12546..7ed8eb23 100644 --- a/tests/src/mock_ssv.rs +++ b/tests/src/mock_ssv.rs @@ -37,7 +37,10 @@ pub async fn create_mock_ssv_server( force_timeout: Arc::new(RwLock::new(false)), }); let router = axum::Router::new() - .route("/{chain_name}/validators/in_operator/{node_operator_id}", get(handle_validators)) + .route( + "/api/v4/{chain_name}/validators/in_operator/{node_operator_id}", + get(handle_validators), + ) .route("/big_data", get(handle_big_data)) .with_state(state) .into_make_service(); diff --git a/tests/tests/pbs_mux.rs b/tests/tests/pbs_mux.rs index 5cde1158..3a15b49b 100644 --- a/tests/tests/pbs_mux.rs +++ b/tests/tests/pbs_mux.rs @@ -29,8 +29,9 @@ async fn test_ssv_network_fetch() -> Result<()> { // Start the mock server let port = 30100; let _server_handle = create_mock_ssv_server(port, None).await?; - let url = Url::parse(&format!("http://localhost:{port}/test_chain/validators/in_operator/1")) - .unwrap(); + let url = + Url::parse(&format!("http://localhost:{port}/api/v4/test_chain/validators/in_operator/1")) + .unwrap(); let response = fetch_ssv_pubkeys_from_url(url, Duration::from_secs(HTTP_TIMEOUT_SECONDS_DEFAULT)).await?; @@ -100,8 +101,9 @@ async fn test_ssv_network_fetch_timeout() -> Result<()> { force_timeout: Arc::new(RwLock::new(true)), }; let server_handle = create_mock_ssv_server(port, Some(state)).await?; - let url = Url::parse(&format!("http://localhost:{port}/test_chain/validators/in_operator/1")) - .unwrap(); + let url = + Url::parse(&format!("http://localhost:{port}/api/v4/test_chain/validators/in_operator/1")) + .unwrap(); let response = fetch_ssv_pubkeys_from_url(url, Duration::from_secs(TEST_HTTP_TIMEOUT)).await; // The response should fail due to timeout diff --git a/tests/tests/pbs_mux_refresh.rs b/tests/tests/pbs_mux_refresh.rs index 431fd84d..44979fbe 100644 --- a/tests/tests/pbs_mux_refresh.rs +++ b/tests/tests/pbs_mux_refresh.rs @@ -42,7 +42,8 @@ async fn test_auto_refresh() -> Result<()> { // Start the mock SSV API server let ssv_api_port = pbs_port + 1; - let ssv_api_url = Url::parse(&format!("http://localhost:{ssv_api_port}"))?; + // Intentionally missing a trailing slash to ensure this is handled properly + let ssv_api_url = Url::parse(&format!("http://localhost:{ssv_api_port}/api/v4"))?; let mock_ssv_state = SsvMockState { validators: Arc::new(RwLock::new(vec![SSVValidator { pubkey: existing_mux_pubkey.clone(),