1use std::sync::{Arc, Mutex};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT, AUTHORIZATION, USER_AGENT};
7use reqwest::{Method, StatusCode};
8use serde::de::DeserializeOwned;
9use serde_json::{Map, Value};
10
11use crate::builder::{ClientBuilder, Config, TokenOptions};
12use crate::error::{normalize_api_error, Error, Result};
13use crate::token::{
14 decode_token, generate_token, verify_token, AccessTokenBuilder, GenerateTokenParams,
15 TokenClaims,
16};
17
18pub const DEFAULT_BASE_URL: &str = "https://api.videosdk.live";
20
21pub const VERSION: &str = env!("CARGO_PKG_VERSION");
23
24const USER_AGENT_VALUE: &str = concat!("videosdk-rs/", env!("CARGO_PKG_VERSION"));
25
26const TOKEN_REFRESH_BUFFER: i64 = 60;
28const MAX_BACKOFF: Duration = Duration::from_secs(8);
29
30pub(crate) type BackoffFn = Arc<dyn Fn(u32) -> Duration + Send + Sync>;
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum Expect {
35 #[default]
37 Json,
38 Text,
40 None,
42}
43
44pub(crate) enum Body {
46 Json(Value),
47 Raw(Vec<u8>),
50}
51
52#[derive(Default)]
54pub(crate) struct CallOptions {
55 pub(crate) query: Vec<(String, String)>,
56 pub(crate) body: Option<Body>,
57 pub(crate) headers: Vec<(String, String)>,
58 pub(crate) expect: Expect,
59}
60
61impl CallOptions {
62 pub(crate) fn new() -> Self {
63 Self::default()
64 }
65
66 pub(crate) fn json(body: impl serde::Serialize) -> Result<Self> {
67 let body = serde_json::to_value(body).map_err(|e| Error::Encode { source: e })?;
68 Ok(Self {
69 body: Some(Body::Json(body)),
70 ..Default::default()
71 })
72 }
73
74 pub(crate) fn query(mut self, query: Vec<(String, String)>) -> Self {
75 self.query = query;
76 self
77 }
78
79 pub(crate) fn expect(mut self, expect: Expect) -> Self {
80 self.expect = expect;
81 self
82 }
83}
84
85#[derive(Default)]
86pub(crate) struct TokenCache {
87 pub(crate) token: Option<String>,
88 pub(crate) expires_at: Option<i64>,
89}
90
91pub(crate) struct ClientInner {
92 pub(crate) config: Config,
93 pub(crate) http: reqwest::Client,
94 pub(crate) base_url: String,
95 pub(crate) can_refresh: bool,
96 pub(crate) token: Mutex<TokenCache>,
97 pub(crate) backoff: BackoffFn,
98}
99
100#[derive(Clone, Default)]
102pub(crate) struct Overrides {
103 pub(crate) max_retries: Option<u32>,
104 pub(crate) timeout: Option<Duration>,
105}
106
107#[derive(Clone)]
122pub struct Client {
123 inner: Arc<ClientInner>,
124 overrides: Overrides,
125}
126
127impl std::fmt::Debug for Client {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("Client")
130 .field("base_url", &self.inner.base_url)
131 .finish_non_exhaustive()
132 }
133}
134
135impl Client {
136 pub fn new() -> Result<Self> {
139 ClientBuilder::new().build()
140 }
141
142 pub fn builder() -> ClientBuilder {
144 ClientBuilder::new()
145 }
146
147 pub(crate) fn from_parts(inner: Arc<ClientInner>, overrides: Overrides) -> Self {
148 Self { inner, overrides }
149 }
150
151 pub fn base_url(&self) -> &str {
153 &self.inner.base_url
154 }
155
156 pub fn with_max_retries(&self, max_retries: u32) -> Self {
160 let mut client = self.clone();
161 client.overrides.max_retries = Some(max_retries);
162 client
163 }
164
165 pub fn with_request_timeout(&self, timeout: Duration) -> Self {
168 let mut client = self.clone();
169 client.overrides.timeout = Some(timeout);
170 client
171 }
172
173 fn max_retries(&self) -> u32 {
174 self.overrides
175 .max_retries
176 .unwrap_or(self.inner.config.max_retries)
177 }
178
179 fn timeout(&self) -> Duration {
180 self.overrides.timeout.unwrap_or(self.inner.config.timeout)
181 }
182
183 fn credentials(&self, what: &str) -> Result<(&str, &str)> {
186 match (
187 self.inner.config.api_key.as_deref(),
188 self.inner.config.secret.as_deref(),
189 ) {
190 (Some(api_key), Some(secret)) => Ok((api_key, secret)),
191 _ => Err(Error::config(format!(
192 "{what} requires the client to be constructed with an API key and secret"
193 ))),
194 }
195 }
196
197 pub fn generate_token(&self) -> Result<String> {
200 let (api_key, secret) = self.credentials("generate_token")?;
201 let TokenOptions {
202 permissions,
203 roles,
204 version,
205 expires_in,
206 } = &self.inner.config.token_options;
207 generate_token(&GenerateTokenParams {
208 api_key: api_key.to_string(),
209 secret_key: secret.to_string(),
210 permissions: permissions.clone(),
211 roles: roles.clone(),
212 version: *version,
213 expires_in: *expires_in,
214 claims: Map::new(),
215 })
216 }
217
218 pub fn access_token(&self) -> Result<AccessTokenBuilder> {
223 let (api_key, secret) = self.credentials("access_token")?;
224 let mut builder = AccessTokenBuilder::new(api_key, secret);
225 if let Some(ttl) = self.inner.config.token_options.expires_in {
226 builder = builder.expires_in(ttl);
227 }
228 Ok(builder)
229 }
230
231 pub fn verify_token(&self, token: &str) -> Result<TokenClaims> {
233 let secret = self.inner.config.secret.as_deref().ok_or_else(|| {
234 Error::config("verify_token requires the client to be constructed with a secret")
235 })?;
236 verify_token(token, secret)
237 }
238
239 pub(crate) fn mint_api_token(&self, ttl: Duration) -> Result<String> {
242 let ttl = if ttl.is_zero() {
243 Duration::from_secs(3600)
244 } else {
245 ttl
246 };
247 self.access_token()?.for_api().expires_in(ttl).to_jwt()
248 }
249
250 fn resolve_token(&self) -> Result<String> {
251 let mut cache = self
252 .inner
253 .token
254 .lock()
255 .unwrap_or_else(|poisoned| poisoned.into_inner());
256
257 if let Some(token) = &cache.token {
258 let fresh = match cache.expires_at {
259 None => true,
260 Some(exp) => exp - TOKEN_REFRESH_BUFFER > unix_now(),
261 };
262 if fresh {
263 return Ok(token.clone());
264 }
265 }
266
267 if !self.inner.can_refresh {
268 return cache.token.clone().ok_or_else(|| {
270 Error::config("no token available and the client cannot generate one")
271 });
272 }
273
274 let token = self.generate_token()?;
275 cache.expires_at = decode_token(&token).ok().and_then(|c| c.expires_at);
276 cache.token = Some(token.clone());
277 Ok(token)
278 }
279
280 pub(crate) async fn execute(
285 &self,
286 method: Method,
287 path: &str,
288 options: &CallOptions,
289 ) -> Result<Vec<u8>> {
290 let idempotent = is_idempotent(&method);
291 let max_retries = self.max_retries();
292
293 let mut attempt = 0u32;
294 loop {
295 match self.attempt(&method, path, options).await {
296 Ok(body) => return Ok(body),
297 Err(err) => {
298 if !should_retry(&err, idempotent) || attempt >= max_retries {
299 return Err(err);
300 }
301 let delay = err
303 .retry_after()
304 .filter(|_| err.status() == Some(429))
305 .unwrap_or_else(|| (self.inner.backoff)(attempt));
306 tokio::time::sleep(delay).await;
307 attempt += 1;
308 }
309 }
310 }
311 }
312
313 fn build_headers(&self, token: &str, options: &CallOptions) -> Result<HeaderMap> {
316 let mut headers = HeaderMap::new();
317
318 let mut authorization = HeaderValue::from_str(token)
320 .map_err(|_| Error::config("the token contains characters invalid in a header"))?;
321 authorization.set_sensitive(true);
322 headers.insert(AUTHORIZATION, authorization);
323
324 let accept = match options.expect {
325 Expect::Text => "*/*",
327 Expect::Json | Expect::None => "application/json",
328 };
329 headers.insert(ACCEPT, HeaderValue::from_static(accept));
330 headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));
331
332 for (name, value) in &self.inner.config.headers {
333 headers.insert(name.clone(), value.clone());
334 }
335 for (name, value) in &options.headers {
336 let name = HeaderName::try_from(name.as_str())
337 .map_err(|_| Error::validation(format!("invalid header name {name:?}")))?;
338 let value = HeaderValue::try_from(value.as_str())
339 .map_err(|_| Error::validation(format!("invalid value for header {name:?}")))?;
340 headers.insert(name, value);
341 }
342
343 Ok(headers)
344 }
345
346 async fn attempt(&self, method: &Method, path: &str, options: &CallOptions) -> Result<Vec<u8>> {
347 let token = self.resolve_token()?;
348 let url = format!("{}{}", self.inner.base_url, ensure_leading_slash(path));
349
350 let mut request = self.inner.http.request(method.clone(), &url);
351 if !options.query.is_empty() {
352 request = request.query(&options.query);
353 }
354
355 request = request.headers(self.build_headers(&token, options)?);
359
360 match &options.body {
361 Some(Body::Json(value)) => request = request.json(value),
364 Some(Body::Raw(bytes)) => request = request.body(bytes.clone()),
365 None => {}
366 }
367
368 let timeout = self.timeout();
369 if !timeout.is_zero() {
370 request = request.timeout(timeout);
371 }
372
373 let response = request
374 .send()
375 .await
376 .map_err(|e| transport_error(e, method, path, timeout))?;
377
378 let status = response.status();
379 let request_id = read_request_id(response.headers());
380 let retry_after = (status == StatusCode::TOO_MANY_REQUESTS)
381 .then(|| parse_retry_after(response.headers()))
382 .flatten();
383
384 let body = response
385 .bytes()
386 .await
387 .map_err(|e| transport_error(e, method, path, timeout))?;
388
389 if !status.is_success() {
390 return Err(Error::api(normalize_api_error(
391 status.as_u16(),
392 parse_maybe_json(&body),
393 method.as_str(),
394 path,
395 request_id,
396 retry_after,
397 )));
398 }
399
400 if options.expect == Expect::None || status == StatusCode::NO_CONTENT {
401 return Ok(Vec::new());
402 }
403 Ok(body.to_vec())
404 }
405
406 pub(crate) async fn put_binary(
410 &self,
411 url: &str,
412 body: &[u8],
413 content_type: &str,
414 ) -> Result<()> {
415 let mut attempt = 0u32;
416 loop {
417 match self.attempt_put(url, body, content_type).await {
418 Ok(()) => return Ok(()),
419 Err(err) => {
420 if !should_retry(&err, true) || attempt >= self.max_retries() {
421 return Err(err);
422 }
423 tokio::time::sleep((self.inner.backoff)(attempt)).await;
424 attempt += 1;
425 }
426 }
427 }
428 }
429
430 async fn attempt_put(&self, url: &str, body: &[u8], content_type: &str) -> Result<()> {
431 let path = safe_url_path(url);
433 let timeout = self.timeout();
434
435 let mut request = self
436 .inner
437 .http
438 .put(url)
439 .header(reqwest::header::CONTENT_TYPE, content_type)
440 .body(body.to_vec());
441 if !timeout.is_zero() {
442 request = request.timeout(timeout);
443 }
444
445 let response = request
446 .send()
447 .await
448 .map_err(|e| transport_error(e, &Method::PUT, path, timeout))?;
449
450 let status = response.status();
451 if status.is_success() {
452 return Ok(());
453 }
454
455 let body = response.bytes().await.unwrap_or_default();
456 let mut api_error = normalize_api_error(
457 status.as_u16(),
458 parse_maybe_json(&body),
459 Method::PUT.as_str(),
460 path,
461 None,
462 None,
463 );
464 api_error.message = format!("uploading the file to storage failed (HTTP {status})");
465 api_error.code = Some("upload_failed".to_string());
466 Err(Error::api(api_error))
467 }
468
469 fn decode<T: DeserializeOwned>(bytes: &[u8], method: &Method, path: &str) -> Result<T> {
470 let context = format!("{method} {path}");
471 if bytes.iter().all(u8::is_ascii_whitespace) {
472 return serde_json::from_str("null").map_err(|e| Error::decode(context, e));
475 }
476 serde_json::from_slice(bytes).map_err(|e| Error::decode(context, e))
477 }
478}
479
480impl Client {
487 pub(crate) async fn json<T: DeserializeOwned>(
489 &self,
490 method: Method,
491 path: &str,
492 options: CallOptions,
493 ) -> Result<T> {
494 let bytes = self.execute(method.clone(), path, &options).await?;
495 Self::decode(&bytes, &method, path)
496 }
497
498 pub(crate) async fn maybe_json(
504 &self,
505 method: Method,
506 path: &str,
507 options: CallOptions,
508 ) -> Result<Value> {
509 let bytes = self.execute(method, path, &options).await?;
510 Ok(parse_maybe_json(&bytes).unwrap_or(Value::Null))
511 }
512
513 pub(crate) async fn data<T: DeserializeOwned>(
515 &self,
516 method: Method,
517 path: &str,
518 options: CallOptions,
519 ) -> Result<T> {
520 self.wrapped(method, path, "data", options).await
521 }
522
523 pub(crate) async fn wrapped<T: DeserializeOwned>(
527 &self,
528 method: Method,
529 path: &str,
530 key: &str,
531 options: CallOptions,
532 ) -> Result<T> {
533 let bytes = self.execute(method.clone(), path, &options).await?;
534 let context = format!("{method} {path}");
535 if bytes.iter().all(u8::is_ascii_whitespace) {
536 return serde_json::from_str("null").map_err(|e| Error::decode(context, e));
537 }
538 let mut envelope: Map<String, Value> =
539 serde_json::from_slice(&bytes).map_err(|e| Error::decode(&context, e))?;
540 let inner = envelope.remove(key).unwrap_or(Value::Null);
541 serde_json::from_value(inner).map_err(|e| Error::decode(context, e))
542 }
543
544 pub(crate) async fn message(
548 &self,
549 method: Method,
550 path: &str,
551 options: CallOptions,
552 ) -> Result<String> {
553 let bytes = self.execute(method, path, &options).await?;
554 let text = String::from_utf8_lossy(&bytes);
555 let trimmed = text.trim();
556 if trimmed.is_empty() {
557 return Ok(String::new());
558 }
559 if trimmed.starts_with('"') {
560 if let Ok(unquoted) = serde_json::from_str::<String>(trimmed) {
561 return Ok(unquoted);
562 }
563 }
564 Ok(trimmed.to_string())
565 }
566
567 pub(crate) async fn text(
569 &self,
570 method: Method,
571 path: &str,
572 options: CallOptions,
573 ) -> Result<String> {
574 let bytes = self
575 .execute(method, path, &options.expect(Expect::Text))
576 .await?;
577 Ok(String::from_utf8_lossy(&bytes).into_owned())
578 }
579
580 pub(crate) async fn none(
582 &self,
583 method: Method,
584 path: &str,
585 options: CallOptions,
586 ) -> Result<()> {
587 self.execute(method, path, &options.expect(Expect::None))
588 .await?;
589 Ok(())
590 }
591}
592
593impl Client {
596 pub async fn request(&self, method: Method, path: &str, request: RawRequest) -> Result<Value> {
601 let expect = request.expect;
602 let body = match (request.raw_body, request.body) {
604 (Some(bytes), _) => Some(Body::Raw(bytes)),
605 (None, Some(value)) => Some(Body::Json(value)),
606 (None, None) => None,
607 };
608 let options = CallOptions {
609 query: request.query,
610 body,
611 headers: request.headers,
612 expect,
613 };
614 let bytes = self.execute(method.clone(), path, &options).await?;
615 match expect {
616 Expect::None => Ok(Value::Null),
617 Expect::Text => Ok(Value::String(String::from_utf8_lossy(&bytes).into_owned())),
620 Expect::Json if bytes.iter().all(u8::is_ascii_whitespace) => Ok(Value::Null),
621 Expect::Json => Self::decode(&bytes, &method, path),
622 }
623 }
624
625 pub fn api(&self) -> Api<'_> {
628 Api { client: self }
629 }
630}
631
632#[derive(Debug, Default)]
634pub struct RawRequest {
635 pub query: Vec<(String, String)>,
637 pub body: Option<Value>,
639 pub raw_body: Option<Vec<u8>>,
644 pub headers: Vec<(String, String)>,
646 pub expect: Expect,
648}
649
650impl RawRequest {
651 pub fn new() -> Self {
653 Self::default()
654 }
655
656 pub fn body(mut self, body: Value) -> Self {
658 self.body = Some(body);
659 self
660 }
661
662 pub fn raw_body(mut self, body: impl Into<Vec<u8>>) -> Self {
673 self.raw_body = Some(body.into());
674 self
675 }
676
677 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
679 self.headers.push((name.into(), value.into()));
680 self
681 }
682
683 pub fn query(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
685 self.query.push((name.into(), value.into()));
686 self
687 }
688
689 pub fn expect(mut self, expect: Expect) -> Self {
691 self.expect = expect;
692 self
693 }
694}
695
696#[derive(Debug, Clone, Copy)]
698pub struct Api<'a> {
699 client: &'a Client,
700}
701
702macro_rules! api_method {
703 ($name:ident, $method:expr, $doc:literal) => {
704 #[doc = $doc]
705 pub async fn $name(&self, path: &str, request: RawRequest) -> Result<Value> {
706 self.client.request($method, path, request).await
707 }
708 };
709}
710
711impl Api<'_> {
712 api_method!(get, Method::GET, "Issues a raw `GET`.");
713 api_method!(post, Method::POST, "Issues a raw `POST`.");
714 api_method!(put, Method::PUT, "Issues a raw `PUT`.");
715 api_method!(patch, Method::PATCH, "Issues a raw `PATCH`.");
716 api_method!(delete, Method::DELETE, "Issues a raw `DELETE`.");
717}
718
719fn unix_now() -> i64 {
722 SystemTime::now()
723 .duration_since(UNIX_EPOCH)
724 .map(|d| d.as_secs() as i64)
725 .unwrap_or(0)
726}
727
728fn ensure_leading_slash(path: &str) -> String {
729 if path.starts_with('/') {
730 path.to_string()
731 } else {
732 format!("/{path}")
733 }
734}
735
736fn safe_url_path(url: &str) -> &str {
738 url.split('?').next().unwrap_or(url)
739}
740
741fn transport_error(
742 source: reqwest::Error,
743 method: &Method,
744 path: &str,
745 timeout: Duration,
746) -> Error {
747 if source.is_timeout() {
748 Error::Timeout {
749 method: method.to_string(),
750 path: path.to_string(),
751 elapsed: timeout,
752 }
753 } else {
754 Error::Network {
755 method: method.to_string(),
756 path: path.to_string(),
757 source,
758 }
759 }
760}
761
762fn parse_maybe_json(body: &[u8]) -> Option<Value> {
765 if body.iter().all(u8::is_ascii_whitespace) {
766 return None;
767 }
768 match serde_json::from_slice(body) {
769 Ok(value) => Some(value),
770 Err(_) => Some(Value::String(String::from_utf8_lossy(body).into_owned())),
771 }
772}
773
774fn read_request_id(headers: &HeaderMap) -> Option<String> {
775 [
776 "x-request-id",
777 "request-id",
778 "x-amzn-requestid",
779 "x-amz-request-id",
780 ]
781 .iter()
782 .find_map(|name| headers.get(*name))
783 .and_then(|value| value.to_str().ok())
784 .filter(|value| !value.is_empty())
785 .map(str::to_string)
786}
787
788fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
790 let value = headers.get("retry-after")?.to_str().ok()?.trim();
791 if value.is_empty() {
792 return None;
793 }
794 if let Ok(seconds) = value.parse::<i64>() {
795 return (seconds > 0).then(|| Duration::from_secs(seconds as u64));
796 }
797 let deadline = httpdate::parse_http_date(value).ok()?;
798 deadline.duration_since(SystemTime::now()).ok()
799}
800
801fn is_idempotent(method: &Method) -> bool {
802 matches!(
803 *method,
804 Method::GET | Method::PUT | Method::DELETE | Method::HEAD | Method::OPTIONS
805 )
806}
807
808fn should_retry(error: &Error, idempotent: bool) -> bool {
814 if error.status() == Some(429) {
815 return true;
816 }
817 if !idempotent {
818 return false;
819 }
820 match error {
821 Error::Api(api) => api.status >= 500,
822 Error::Network { .. } | Error::Timeout { .. } => true,
823 _ => false,
824 }
825}
826
827pub(crate) fn default_backoff(attempt: u32) -> Duration {
829 let base = Duration::from_secs(1)
830 .saturating_mul(1u32.checked_shl(attempt).unwrap_or(u32::MAX))
831 .min(MAX_BACKOFF);
832 base + Duration::from_millis(jitter_ms())
833}
834
835fn jitter_ms() -> u64 {
838 let nanos = SystemTime::now()
839 .duration_since(UNIX_EPOCH)
840 .map(|d| d.subsec_nanos() as u64)
841 .unwrap_or(0);
842 let mut x = nanos
843 .wrapping_mul(6_364_136_223_846_793_005)
844 .wrapping_add(1_442_695_040_888_963_407);
845 x ^= x >> 33;
846 x = x.wrapping_mul(0xff51_afd7_ed55_8ccd);
847 x ^= x >> 33;
848 x % 250
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854 use crate::error::ApiError;
855 use crate::error::ErrorKind;
856
857 fn api_error(status: u16) -> Error {
858 Error::api(ApiError {
859 message: String::new(),
860 kind: ErrorKind::from_status(status),
861 code: None,
862 status,
863 request_id: None,
864 details: None,
865 method: "GET".into(),
866 path: "/x".into(),
867 retry_after: None,
868 })
869 }
870
871 #[test]
872 fn idempotent_methods() {
873 for method in [
874 Method::GET,
875 Method::PUT,
876 Method::DELETE,
877 Method::HEAD,
878 Method::OPTIONS,
879 ] {
880 assert!(is_idempotent(&method), "{method} should be idempotent");
881 }
882 assert!(!is_idempotent(&Method::POST));
883 assert!(!is_idempotent(&Method::PATCH));
884 }
885
886 #[test]
887 fn rate_limits_retry_for_any_method() {
888 assert!(should_retry(&api_error(429), true));
889 assert!(should_retry(&api_error(429), false));
890 }
891
892 #[test]
893 fn server_errors_retry_only_when_idempotent() {
894 assert!(should_retry(&api_error(500), true));
895 assert!(should_retry(&api_error(503), true));
896 assert!(!should_retry(&api_error(500), false));
897 }
898
899 #[test]
900 fn client_errors_never_retry() {
901 for status in [400, 401, 403, 404, 409] {
902 assert!(!should_retry(&api_error(status), true), "status {status}");
903 }
904 }
905
906 #[test]
907 fn timeouts_retry_only_when_idempotent() {
908 let err = Error::Timeout {
909 method: "GET".into(),
910 path: "/x".into(),
911 elapsed: Duration::from_secs(1),
912 };
913 assert!(should_retry(&err, true));
914 assert!(!should_retry(&err, false));
915 }
916
917 #[test]
918 fn config_and_decode_errors_never_retry() {
919 assert!(!should_retry(&Error::config("x"), true));
920 assert!(!should_retry(&Error::validation("x"), true));
921 }
922
923 #[test]
924 fn backoff_grows_exponentially_and_caps() {
925 let bounds = |attempt: u32| {
926 let d = default_backoff(attempt);
927 (d.as_millis() as u64) / 1000
928 };
929 assert_eq!(bounds(0), 1);
930 assert_eq!(bounds(1), 2);
931 assert_eq!(bounds(2), 4);
932 assert_eq!(bounds(3), 8);
933 assert!(default_backoff(10) < Duration::from_millis(8_250));
935 assert!(default_backoff(64) < Duration::from_millis(8_250));
936 }
937
938 #[test]
939 fn jitter_stays_in_range() {
940 for _ in 0..100 {
941 assert!(jitter_ms() < 250);
942 }
943 }
944
945 #[test]
946 fn parses_retry_after_seconds() {
947 let mut headers = HeaderMap::new();
948 headers.insert("retry-after", "3".parse().unwrap());
949 assert_eq!(parse_retry_after(&headers), Some(Duration::from_secs(3)));
950 }
951
952 #[test]
953 fn ignores_non_positive_or_missing_retry_after() {
954 assert_eq!(parse_retry_after(&HeaderMap::new()), None);
955 let mut headers = HeaderMap::new();
956 headers.insert("retry-after", "0".parse().unwrap());
957 assert_eq!(parse_retry_after(&headers), None);
958 headers.insert("retry-after", "-5".parse().unwrap());
959 assert_eq!(parse_retry_after(&headers), None);
960 headers.insert("retry-after", "garbage".parse().unwrap());
961 assert_eq!(parse_retry_after(&headers), None);
962 }
963
964 #[test]
965 fn parses_retry_after_http_date() {
966 let future = SystemTime::now() + Duration::from_secs(120);
967 let mut headers = HeaderMap::new();
968 headers.insert(
969 "retry-after",
970 httpdate::fmt_http_date(future).parse().unwrap(),
971 );
972 let parsed = parse_retry_after(&headers).expect("should parse an HTTP-date");
973 assert!(parsed > Duration::from_secs(60) && parsed <= Duration::from_secs(120));
974
975 let past = SystemTime::now() - Duration::from_secs(60);
977 headers.insert(
978 "retry-after",
979 httpdate::fmt_http_date(past).parse().unwrap(),
980 );
981 assert_eq!(parse_retry_after(&headers), None);
982 }
983
984 #[test]
985 fn reads_request_id_in_priority_order() {
986 let mut headers = HeaderMap::new();
987 assert_eq!(read_request_id(&headers), None);
988 headers.insert("x-amz-request-id", "amz".parse().unwrap());
989 assert_eq!(read_request_id(&headers).as_deref(), Some("amz"));
990 headers.insert("x-request-id", "primary".parse().unwrap());
991 assert_eq!(read_request_id(&headers).as_deref(), Some("primary"));
992 }
993
994 #[test]
995 fn parse_maybe_json_falls_back_to_text() {
996 assert_eq!(parse_maybe_json(b" "), None);
997 assert_eq!(
998 parse_maybe_json(b"{\"a\":1}"),
999 Some(serde_json::json!({"a": 1}))
1000 );
1001 assert_eq!(
1002 parse_maybe_json(b"<html>oops</html>"),
1003 Some(Value::String("<html>oops</html>".into()))
1004 );
1005 }
1006
1007 #[test]
1008 fn safe_url_path_strips_the_signature() {
1009 assert_eq!(
1010 safe_url_path("https://s3/bucket/key?X-Amz-Signature=secret"),
1011 "https://s3/bucket/key"
1012 );
1013 assert_eq!(safe_url_path("https://s3/key"), "https://s3/key");
1014 }
1015
1016 #[test]
1017 fn ensures_a_leading_slash() {
1018 assert_eq!(ensure_leading_slash("v2/rooms"), "/v2/rooms");
1019 assert_eq!(ensure_leading_slash("/v2/rooms"), "/v2/rooms");
1020 }
1021}