1use std::{fmt::Display, time::Duration};
16
17use http::{
18 header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT},
19 StatusCode,
20};
21use serde::Deserialize;
22use thiserror::Error;
23use url::Url;
24
25use super::failover::{self, FailoverConfig};
26use crate::http_client;
27
28pub const DEFAULT_PREFIX: &str = "/twirp";
29
30const USER_AGENT_VALUE: &str = concat!("livekit-server-sdk-rust/", env!("CARGO_PKG_VERSION"));
32
33fn normalize_host(host: &str) -> String {
36 if let Some(rest) = host.strip_prefix("wss://") {
37 format!("https://{rest}")
38 } else if let Some(rest) = host.strip_prefix("ws://") {
39 format!("http://{rest}")
40 } else {
41 host.to_owned()
42 }
43}
44
45#[cfg(test)]
46mod normalize_host_tests {
47 use super::normalize_host;
48
49 #[test]
50 fn normalizes_ws_schemes() {
51 assert_eq!(normalize_host("wss://my.livekit.cloud"), "https://my.livekit.cloud");
52 assert_eq!(normalize_host("ws://localhost:7880"), "http://localhost:7880");
53 assert_eq!(normalize_host("https://my.livekit.cloud"), "https://my.livekit.cloud");
54 assert_eq!(normalize_host("http://localhost:7880"), "http://localhost:7880");
55 }
56}
57
58#[derive(Debug, Error)]
59pub enum ServerError {
60 #[cfg(feature = "services-tokio")]
61 #[error("failed to execute the request: {0}")]
62 Request(#[from] reqwest::Error),
63 #[cfg(feature = "services-async")]
64 #[error("failed to execute the request: {0}")]
65 Request(#[from] std::io::Error),
66 #[error("server error: {0}")]
67 Twirp(ServerErrorCode),
68 #[error("url error: {0}")]
69 Url(#[from] url::ParseError),
70 #[error("prost error: {0}")]
71 Prost(#[from] prost::DecodeError),
72}
73
74pub type TwirpError = ServerError;
76
77#[derive(Debug, Deserialize)]
78pub struct ServerErrorCode {
79 pub code: String,
80 pub msg: String,
81 #[serde(default)]
84 pub meta: std::collections::HashMap<String, String>,
85}
86
87impl ServerErrorCode {
88 pub const CANCELED: &'static str = "canceled";
89 pub const UNKNOWN: &'static str = "unknown";
90 pub const INVALID_ARGUMENT: &'static str = "invalid_argument";
91 pub const MALFORMED: &'static str = "malformed";
92 pub const DEADLINE_EXCEEDED: &'static str = "deadline_exceeded";
93 pub const NOT_FOUND: &'static str = "not_found";
94 pub const BAD_ROUTE: &'static str = "bad_route";
95 pub const ALREADY_EXISTS: &'static str = "already_exists";
96 pub const PERMISSION_DENIED: &'static str = "permission_denied";
97 pub const UNAUTHENTICATED: &'static str = "unauthenticated";
98 pub const RESOURCE_EXHAUSTED: &'static str = "resource_exhausted";
99 pub const FAILED_PRECONDITION: &'static str = "failed_precondition";
100 pub const ABORTED: &'static str = "aborted";
101 pub const OUT_OF_RANGE: &'static str = "out_of_range";
102 pub const UNIMPLEMENTED: &'static str = "unimplemented";
103 pub const INTERNAL: &'static str = "internal";
104 pub const UNAVAILABLE: &'static str = "unavailable";
105 pub const DATA_LOSS: &'static str = "dataloss";
106}
107
108impl Display for ServerErrorCode {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 write!(f, "{}: {}", self.code, self.msg)
111 }
112}
113
114pub type ServerResult<T> = Result<T, ServerError>;
115
116pub type TwirpErrorCode = ServerErrorCode;
118
119pub type TwirpResult<T> = ServerResult<T>;
121
122#[derive(Debug)]
123pub struct TwirpClient {
124 host: String,
125 pkg: String,
126 prefix: String,
127 client: http_client::Client,
128 failover: FailoverConfig,
129 request_timeout: Duration,
130 #[cfg(test)]
133 default_headers: HeaderMap,
134}
135
136impl TwirpClient {
137 pub fn new(host: &str, pkg: &str, prefix: Option<&str>) -> Self {
138 Self::with_client(host, pkg, prefix, http_client::Client::new())
139 }
140
141 pub(crate) fn with_client(
147 host: &str,
148 pkg: &str,
149 prefix: Option<&str>,
150 client: http_client::Client,
151 ) -> Self {
152 Self {
153 host: normalize_host(host),
154 pkg: pkg.to_owned(),
155 prefix: prefix.unwrap_or(DEFAULT_PREFIX).to_owned(),
156 client,
157 failover: FailoverConfig::default(),
158 request_timeout: failover::DEFAULT_REQUEST_TIMEOUT,
159 #[cfg(test)]
160 default_headers: HeaderMap::new(),
161 }
162 }
163
164 #[cfg(test)]
165 pub(crate) fn with_default_headers(mut self, headers: HeaderMap) -> Self {
166 self.default_headers = headers;
167 self
168 }
169
170 pub fn with_failover(mut self, enabled: bool) -> Self {
173 self.failover.enabled = enabled;
174 self
175 }
176
177 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
180 self.request_timeout = timeout;
181 self
182 }
183
184 #[cfg(test)]
187 pub(crate) fn with_failover_config(mut self, config: FailoverConfig) -> Self {
188 self.failover = config;
189 self
190 }
191
192 pub async fn request<D: prost::Message, R: prost::Message + Default>(
198 &self,
199 service: &str,
200 method: &str,
201 data: D,
202 headers: HeaderMap,
203 ) -> ServerResult<R> {
204 self.request_with_timeout(service, method, data, headers, self.request_timeout).await
205 }
206
207 pub async fn request_with_timeout<D: prost::Message, R: prost::Message + Default>(
210 &self,
211 service: &str,
212 method: &str,
213 data: D,
214 mut headers: HeaderMap,
215 timeout: Duration,
216 ) -> ServerResult<R> {
217 let original = Url::parse(&self.host)?;
218 let path = format!("{}/{}.{}/{}", self.prefix, self.pkg, service, method);
219 headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
220 #[cfg(test)]
221 for (k, v) in &self.default_headers {
222 headers.insert(k.clone(), v.clone());
223 }
224 let forward = headers.clone(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/protobuf"));
226 let body = data.encode_to_vec();
227
228 let max_attempts = self.failover.attempts(original.host_str(), timeout);
229 let mut attempted = vec![failover::host_key(&original)];
230 let mut region_urls: Option<Vec<String>> = None;
231 let mut current = original.clone();
232
233 for attempt in 0..max_attempts {
234 let is_last = attempt + 1 >= max_attempts;
235 let mut url = current.clone();
236 url.set_path(&path);
237
238 let send = self
239 .client
240 .post(url)
241 .headers(headers.clone())
242 .body(body.clone())
243 .timeout(timeout)
244 .send()
245 .await;
246 let (next, reason) = match send {
249 Ok(resp) => {
250 let status = resp.status();
251 if status == StatusCode::OK {
252 return Ok(R::decode(resp.bytes().await?)?);
253 }
254 let next = if is_last || status.as_u16() < 500 {
256 None
257 } else {
258 self.next_region(&original, &forward, &mut region_urls, &attempted).await
259 };
260 let Some(next) = next else {
262 let err: ServerErrorCode = resp.json().await?;
263 return Err(ServerError::Twirp(err));
264 };
265 drop(resp); (next, format!("status {status}"))
267 }
268 Err(err) => {
269 let next = if is_last {
270 None
271 } else {
272 self.next_region(&original, &forward, &mut region_urls, &attempted).await
273 };
274 match next {
275 Some(next) => (next, err.to_string()),
276 None => return Err(err.into()),
277 }
278 }
279 };
280
281 log::warn!(
282 "livekit API request to {} failed ({}), retrying with fallback url {}",
283 current.host_str().unwrap_or_default(),
284 reason,
285 next,
286 );
287 failover::backoff_sleep(self.backoff(attempt)).await;
288 attempted.push(failover::host_key(&next));
289 current = next;
290 }
291 unreachable!("failover loop always returns within the attempt budget")
292 }
293
294 fn backoff(&self, attempt: u32) -> std::time::Duration {
295 self.failover.backoff_base * (1u32 << attempt)
296 }
297
298 async fn next_region(
301 &self,
302 original: &Url,
303 forward: &HeaderMap,
304 region_urls: &mut Option<Vec<String>>,
305 attempted: &[String],
306 ) -> Option<Url> {
307 let region_urls = match region_urls {
308 Some(urls) => urls,
309 None => region_urls.insert(failover::region_urls(original, forward).await),
310 };
311 failover::pick_next(region_urls, attempted)
312 }
313}