From 55bf5719c4fe9f2e11bc1dd381462507a1d49140 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 7 Jul 2026 08:58:28 +0200 Subject: [PATCH] Enable using base URLs with path extensions --- .changeset/base-path.md | 5 +++ livekit-api/src/lib.rs | 35 +++++++++++++++++++ livekit-api/src/services/failover.rs | 9 ++--- livekit-api/src/services/mod.rs | 1 + livekit-api/src/services/twirp_client.rs | 9 ++--- .../src/signal_client/region_url_provider.rs | 4 +-- 6 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 .changeset/base-path.md diff --git a/.changeset/base-path.md b/.changeset/base-path.md new file mode 100644 index 000000000..5a26411dc --- /dev/null +++ b/.changeset/base-path.md @@ -0,0 +1,5 @@ +--- +livekit-api: patch +--- + +fix: enable using base URLs with path extensions diff --git a/livekit-api/src/lib.rs b/livekit-api/src/lib.rs index 878da8147..7eb78dd17 100644 --- a/livekit-api/src/lib.rs +++ b/livekit-api/src/lib.rs @@ -14,6 +14,8 @@ #![doc = include_str!("../README.md")] +use url::Url; + #[cfg(feature = "access-token")] pub mod access_token; @@ -60,3 +62,36 @@ pub(crate) fn get_env_keys() -> Result<(String, String), std::env::VarError> { let api_secret = std::env::var("LIVEKIT_API_SECRET")?; Ok((api_key, api_secret)) } + +/// Creates a new URL by appending `suffix` to the supplied `base` URL without overwriting its path. +fn url_with_path_suffix(base: &Url, suffix: &str) -> Url { + let mut path = base.path().trim_end_matches('/').to_owned(); + path.push_str(suffix); + let mut url = base.clone(); + url.set_path(&path); + url +} + +#[cfg(test)] +mod tests { + use crate::url_with_path_suffix; + use url::Url; + + #[test] + fn url_with_path_suffix_no_path_on_base() { + let url = url_with_path_suffix(&Url::parse("https://foo.bar").unwrap(), "/foobar"); + assert_eq!(url.to_string(), "https://foo.bar/foobar"); + } + + #[test] + fn url_with_path_suffix_root_path_on_base() { + let url = url_with_path_suffix(&Url::parse("https://foo.bar/").unwrap(), "/foobar"); + assert_eq!(url.to_string(), "https://foo.bar/foobar"); + } + + #[test] + fn url_with_path_suffix_some_path_on_base() { + let url = url_with_path_suffix(&Url::parse("https://foo.bar/bar").unwrap(), "/foobar"); + assert_eq!(url.to_string(), "https://foo.bar/bar/foobar"); + } +} diff --git a/livekit-api/src/services/failover.rs b/livekit-api/src/services/failover.rs index ab45a6bf3..8d3dd5f4c 100644 --- a/livekit-api/src/services/failover.rs +++ b/livekit-api/src/services/failover.rs @@ -178,8 +178,9 @@ fn max_age_from_cache_control(value: Option<&str>) -> Option { #[cfg(feature = "services-tokio")] async fn fetch(base: &Url, headers: &HeaderMap) -> Result<(Vec, Option), ()> { - let mut url = base.clone(); - url.set_path("/settings/regions"); + use crate::url_with_path_suffix; + + let url = url_with_path_suffix(base, "/settings/regions"); let resp = reqwest::Client::new() .get(url) @@ -200,11 +201,11 @@ async fn fetch(base: &Url, headers: &HeaderMap) -> Result<(Vec, Option Result<(Vec, Option), ()> { + use crate::url_with_path_suffix; use isahc::config::Configurable; use isahc::AsyncReadResponseExt; - let mut url = base.clone(); - url.set_path("/settings/regions"); + let url = url_with_path_suffix(base, "/settings/regions"); let mut builder = isahc::Request::get(url.as_str()).timeout(DISCOVERY_TIMEOUT); // isahc vendors `http` 0.2, so pass name/value as &str/&[u8] to stay agnostic diff --git a/livekit-api/src/services/mod.rs b/livekit-api/src/services/mod.rs index fa470435a..cbbb49f3d 100644 --- a/livekit-api/src/services/mod.rs +++ b/livekit-api/src/services/mod.rs @@ -17,6 +17,7 @@ use std::fmt::{Debug, Display}; use http::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use thiserror::Error; +use url::Url; use crate::access_token::{AccessToken, AccessTokenError, SIPGrants, VideoGrants}; diff --git a/livekit-api/src/services/twirp_client.rs b/livekit-api/src/services/twirp_client.rs index 15b249d28..aae07409b 100644 --- a/livekit-api/src/services/twirp_client.rs +++ b/livekit-api/src/services/twirp_client.rs @@ -23,7 +23,7 @@ use thiserror::Error; use url::Url; use super::failover::{self, FailoverConfig}; -use crate::http_client; +use crate::{http_client, url_with_path_suffix}; pub const DEFAULT_PREFIX: &str = "/twirp"; @@ -215,7 +215,6 @@ impl TwirpClient { timeout: Duration, ) -> ServerResult { let original = Url::parse(&self.host)?; - let path = format!("{}/{}.{}/{}", self.prefix, self.pkg, service, method); headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE)); #[cfg(test)] for (k, v) in &self.default_headers { @@ -232,8 +231,10 @@ impl TwirpClient { for attempt in 0..max_attempts { let is_last = attempt + 1 >= max_attempts; - let mut url = current.clone(); - url.set_path(&path); + let url = url_with_path_suffix( + ¤t, + &format!("{}/{}.{}/{}", self.prefix, self.pkg, service, method), + ); let send = self .client diff --git a/livekit-api/src/signal_client/region_url_provider.rs b/livekit-api/src/signal_client/region_url_provider.rs index c27515aa7..90eb29030 100644 --- a/livekit-api/src/signal_client/region_url_provider.rs +++ b/livekit-api/src/signal_client/region_url_provider.rs @@ -23,8 +23,8 @@ use http::header::{HeaderMap, HeaderValue, AUTHORIZATION, CACHE_CONTROL}; use parking_lot::Mutex; use tokio::sync::Mutex as AsyncMutex; -use crate::http_client; use crate::region::{is_cloud_host, parse_max_age, Cached, RegionCache, RegionsResponse}; +use crate::{http_client, url_with_path_suffix}; use super::{SignalError, SignalResult, REGION_FETCH_TIMEOUT}; @@ -207,7 +207,7 @@ fn region_endpoint(url: &str) -> SignalResult { "ws" => url.set_scheme("http").unwrap(), _ => (), } - url.set_path("/settings/regions"); + let url = url_with_path_suffix(&url, "/settings/regions"); Ok(url.to_string()) }