1use std::fmt::Display;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::time::{Duration, Instant};
5
6use bytes::Bytes;
7use serde::de::DeserializeOwned;
8use tokio::sync::Mutex;
9use url::Url;
10
11use crate::auth::{AuthStrategy, BearerAuth, TokenProvider};
12use crate::cache::{CachedResponse, FileCache, ResponseCache, cache_key};
13use crate::config::Config;
14use crate::error::{Error, ErrorCode, retry_after_seconds};
15use crate::http::header::{
16 ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, IF_NONE_MATCH,
17 PROXY_AUTHORIZATION, USER_AGENT,
18};
19use crate::http::{
20 Body, HeaderMap, HeaderValue, HttpClient, Method, Request, Response as HttpResponse, StatusCode,
21};
22use crate::observability::{
23 Hooks, NoopHooks, OperationInfo, OperationState, RequestInfo, RequestResult,
24};
25use crate::operation::Operation;
26use crate::pagination::Page;
27use crate::route::Route;
28use crate::security::{is_same_origin, require_secure_endpoint};
29use crate::services::boxes::BoxKinds;
30#[cfg(feature = "tracing")]
31use crate::trace::label;
32use crate::trace::{AttemptSpan, OperationSpan};
33use crate::version::default_user_agent;
34
35pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
37pub const DEFAULT_MAX_RETRIES: u32 = 3;
39pub const DEFAULT_BASE_DELAY: Duration = Duration::from_secs(1);
41pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
49pub const DEFAULT_MAX_JITTER: Duration = Duration::from_millis(100);
52pub const DEFAULT_MAX_PAGES: usize = 10_000;
54pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 16 << 20;
56
57pub const MAX_RESPONSE_BODY_BYTES: usize = 50 << 20;
61
62const RETRYABLE_STATUSES: &[u16] = &[429, 500, 502, 503, 504];
65const ACCOUNT_FILTER_PARAMETER: &str = "filtered_account_id";
66const MAX_REDIRECTS: usize = 10;
69
70tokio::task_local! {
71 static DEADLINE: Option<Instant>;
75
76 static ENCLOSING: OperationSpan;
80}
81
82#[derive(Clone)]
87pub struct Client {
88 pub(crate) shared: Arc<Shared>,
89 pub(crate) account_id: Option<i64>,
90 pub(crate) scope: Arc<ScopeState>,
91}
92
93pub(crate) struct Shared {
94 pub(crate) config: Config,
95 pub(crate) base_url: Url,
96 pub(crate) http: Arc<dyn HttpClient>,
97 pub(crate) auth: Arc<dyn AuthStrategy>,
98 pub(crate) bearer_auth: bool,
105 pub(crate) user_agent: String,
106 pub(crate) max_retries: u32,
107 pub(crate) base_delay: Option<Duration>,
108 pub(crate) max_delay: Duration,
109 pub(crate) max_jitter: Duration,
110 pub(crate) max_pages: usize,
111 pub(crate) max_response_body_bytes: usize,
112 pub(crate) cache: Option<Arc<dyn ResponseCache>>,
113 pub(crate) hooks: Arc<dyn Hooks>,
114 pub(crate) operation_timeout: Option<Duration>,
115 pub(crate) refreshes: AtomicU64,
119 pub(crate) refresh_runs: AtomicU64,
123 pub(crate) refreshing: tokio::sync::RwLock<()>,
133 pub(crate) signing: Mutex<Option<HeaderValue>>,
147}
148
149impl Shared {
150 fn generation(&self) -> Generation {
154 Generation {
155 refreshes: self.refreshes.load(Ordering::Acquire),
156 runs: self.refresh_runs.load(Ordering::Acquire),
157 }
158 }
159
160 async fn bearer_now(&self) -> Result<Option<HeaderValue>, Error> {
167 let mut probe = Request::new(Bytes::new());
168 self.auth.authenticate(&mut probe).await?;
169 Ok(probe.headers().get(AUTHORIZATION).cloned())
170 }
171}
172
173#[derive(Default)]
177pub(crate) struct ScopeState {
178 pub(crate) default_sender_id: Mutex<Option<i64>>,
179 pub(crate) account_user_id: Mutex<Option<i64>>,
180 pub(crate) box_kinds: Mutex<Option<BoxKinds>>,
181}
182
183#[derive(Debug, Clone)]
185#[non_exhaustive]
186pub struct Response {
187 pub status: StatusCode,
189 pub headers: HeaderMap,
191 pub body: Bytes,
193 pub url: Url,
195 pub from_cache: bool,
198 pub empty: bool,
201}
202
203impl Response {
204 pub fn json<T: DeserializeOwned>(&self) -> Result<T, Error> {
207 if self.body.is_empty() {
208 let error = Error::api(self.status.as_u16(), "empty response body");
209 Err(match self.header("x-request-id") {
210 Some(request_id) => error.with_request_id(request_id),
211 None => error,
212 })
213 } else {
214 serde_json::from_slice(&self.body).map_err(|error| {
215 Error::decoding(self.status.as_u16(), self.header("x-request-id"), error)
216 })
217 }
218 }
219
220 pub fn header(&self, name: &str) -> Option<&str> {
222 self.headers.get(name).and_then(|value| value.to_str().ok())
223 }
224}
225
226pub struct ClientBuilder {
229 config: Config,
230 auth: Option<Arc<dyn AuthStrategy>>,
231 bearer_auth: bool,
232 http: Option<Arc<dyn HttpClient>>,
233 user_agent: String,
234 timeout: Duration,
235 max_retries: u32,
236 base_delay: Option<Duration>,
237 max_delay: Duration,
238 max_jitter: Duration,
239 max_pages: usize,
240 max_response_body_bytes: usize,
241 cache: Option<Arc<dyn ResponseCache>>,
242 pub(crate) hooks: Arc<dyn Hooks>,
243 operation_timeout: Option<Duration>,
244}
245
246impl ClientBuilder {
247 pub fn new(config: Config) -> ClientBuilder {
249 ClientBuilder {
250 config,
251 auth: None,
252 bearer_auth: false,
253 http: None,
254 user_agent: default_user_agent(),
255 timeout: DEFAULT_TIMEOUT,
256 max_retries: DEFAULT_MAX_RETRIES,
257 base_delay: None,
258 max_delay: DEFAULT_MAX_DELAY,
259 max_jitter: DEFAULT_MAX_JITTER,
260 max_pages: DEFAULT_MAX_PAGES,
261 max_response_body_bytes: DEFAULT_MAX_RESPONSE_BODY_BYTES,
262 cache: None,
263 hooks: Arc::new(NoopHooks),
264 operation_timeout: None,
265 }
266 }
267
268 #[must_use]
270 pub fn token_provider(self, provider: impl TokenProvider + 'static) -> ClientBuilder {
271 let mut builder = self.auth_strategy(BearerAuth::new(provider));
272 builder.bearer_auth = true;
273 builder
274 }
275
276 #[must_use]
278 pub fn auth_strategy(mut self, strategy: impl AuthStrategy + 'static) -> ClientBuilder {
279 self.auth = Some(Arc::new(strategy));
280 self.bearer_auth = false;
281 self
282 }
283
284 #[must_use]
289 pub fn http_client(mut self, http: impl HttpClient + 'static) -> ClientBuilder {
290 self.http = Some(Arc::new(http));
291 self
292 }
293
294 #[must_use]
296 pub fn user_agent(mut self, user_agent: impl Into<String>) -> ClientBuilder {
297 self.user_agent = user_agent.into();
298 self
299 }
300
301 #[must_use]
305 pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
306 self.timeout = timeout;
307 self
308 }
309
310 #[must_use]
319 pub fn operation_timeout(mut self, limit: Duration) -> ClientBuilder {
320 self.operation_timeout = Some(limit);
321 self
322 }
323
324 #[must_use]
329 pub fn max_retries(mut self, max_retries: u32) -> ClientBuilder {
330 self.max_retries = max_retries;
331 self
332 }
333
334 #[must_use]
340 pub fn base_delay(mut self, base_delay: Duration) -> ClientBuilder {
341 self.base_delay = Some(base_delay);
342 self
343 }
344
345 #[must_use]
349 pub fn max_delay(mut self, max_delay: Duration) -> ClientBuilder {
350 self.max_delay = max_delay;
351 self
352 }
353
354 #[must_use]
357 pub fn max_jitter(mut self, max_jitter: Duration) -> ClientBuilder {
358 self.max_jitter = max_jitter;
359 self
360 }
361
362 #[must_use]
365 pub fn max_pages(mut self, max_pages: usize) -> ClientBuilder {
366 self.max_pages = max_pages;
367 self
368 }
369
370 #[must_use]
373 pub fn max_response_body_bytes(mut self, bytes: usize) -> ClientBuilder {
374 self.max_response_body_bytes = bytes;
375 self
376 }
377
378 #[must_use]
381 pub fn cache(mut self, cache: impl ResponseCache + 'static) -> ClientBuilder {
382 self.cache = Some(Arc::new(cache));
383 self
384 }
385
386 #[must_use]
389 pub fn hooks(mut self, hooks: impl Hooks + 'static) -> ClientBuilder {
390 self.hooks = Arc::new(hooks);
391 self
392 }
393
394 pub fn build(self) -> Result<Client, Error> {
397 let base_url = parse_base_url(&self.config.base_url)?;
398 let auth = self
399 .auth
400 .ok_or_else(|| Error::usage("a token provider or auth strategy is required"))?;
401 if self.timeout.is_zero() {
402 return Err(Error::usage("timeout must be greater than zero"));
403 }
404 if self.max_pages == 0 {
405 return Err(Error::usage("max pages must be greater than zero"));
406 }
407 if self.operation_timeout.is_some_and(|limit| limit.is_zero()) {
408 return Err(Error::usage("operation timeout must be greater than zero"));
409 }
410 if self
411 .operation_timeout
412 .is_some_and(|limit| Instant::now().checked_add(limit).is_none())
413 {
414 return Err(Error::usage(
415 "operation timeout is too long to keep time by",
416 ));
417 }
418 let http = match self.http {
419 Some(http) => http,
420 None => shipped_http_client(self.timeout)?,
421 };
422 let cache =
423 match (self.cache, self.config.cache_enabled) {
424 (Some(cache), _) => Some(cache),
425 (None, true) => Some(Arc::new(FileCache::new(self.config.cache_dir.clone()))
426 as Arc<dyn ResponseCache>),
427 (None, false) => None,
428 };
429 let max_response_body_bytes = match self.max_response_body_bytes {
430 0 => DEFAULT_MAX_RESPONSE_BODY_BYTES,
431 bytes => bytes,
432 };
433 let shared = Shared {
434 config: self.config,
435 base_url,
436 http,
437 auth,
438 bearer_auth: self.bearer_auth,
439 user_agent: self.user_agent,
440 max_retries: self.max_retries,
441 base_delay: self.base_delay,
442 max_delay: self.max_delay,
443 max_jitter: self.max_jitter,
444 max_pages: self.max_pages,
445 max_response_body_bytes,
446 cache,
447 hooks: self.hooks,
448 operation_timeout: self.operation_timeout,
449 refreshes: AtomicU64::new(0),
450 refresh_runs: AtomicU64::new(0),
451 refreshing: tokio::sync::RwLock::new(()),
452 signing: Mutex::new(None),
453 };
454 Ok(Client {
455 shared: Arc::new(shared),
456 account_id: None,
457 scope: Arc::default(),
458 })
459 }
460}
461
462impl Client {
463 pub fn builder(config: Config) -> ClientBuilder {
465 ClientBuilder::new(config)
466 }
467
468 #[cfg(feature = "reqwest")]
472 #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
473 pub fn new(config: Config, provider: impl TokenProvider + 'static) -> Result<Client, Error> {
474 Client::builder(config).token_provider(provider).build()
475 }
476
477 pub fn config(&self) -> &Config {
479 &self.shared.config
480 }
481
482 pub fn base_url(&self) -> &Url {
484 &self.shared.base_url
485 }
486
487 pub fn account_id(&self) -> Option<i64> {
489 self.account_id
490 }
491
492 pub fn max_pages(&self) -> usize {
494 self.shared.max_pages
495 }
496
497 pub(crate) fn http(&self) -> &dyn HttpClient {
502 self.shared.http.as_ref()
503 }
504
505 pub fn operation(&self, route: &'static Route, params: &[&dyn Display]) -> Operation {
509 Operation::for_route(route, params)
510 }
511
512 pub fn request(&self, method: Method, path: impl Into<String>) -> Operation {
516 Operation::raw(method, path.into())
517 }
518
519 pub async fn send<T: DeserializeOwned>(&self, operation: Operation) -> Result<T, Error> {
521 let label = operation.label().to_string();
522 self.execute(operation)
523 .await?
524 .json()
525 .map_err(|error| error.about(&label))
526 }
527
528 pub async fn send_unit(&self, operation: Operation) -> Result<(), Error> {
530 self.execute(operation).await.map(|_| ())
531 }
532
533 pub async fn send_text(&self, operation: Operation) -> Result<String, Error> {
536 let response = self.execute(operation).await?;
537 Ok(String::from_utf8_lossy(&response.body).into_owned())
538 }
539
540 pub async fn send_optional<T: DeserializeOwned>(
542 &self,
543 operation: Operation,
544 ) -> Result<Option<T>, Error> {
545 let label = operation.label().to_string();
546 let response = self.execute(operation).await?;
547 if response.empty {
548 Ok(None)
549 } else {
550 response
551 .json()
552 .map(Some)
553 .map_err(|error| error.about(&label))
554 }
555 }
556
557 pub async fn send_page<T: DeserializeOwned>(
559 &self,
560 operation: Operation,
561 ) -> Result<Page<T>, Error> {
562 let label = operation.label().to_string();
563 let info = operation.info.clone();
564 let route = operation.route;
565 let response = self.execute(operation).await?;
566 let value = response.json().map_err(|error| error.about(&label))?;
567 Ok(Page::new(value, &response, info, route))
568 }
569
570 pub async fn next_page<T: DeserializeOwned>(
576 &self,
577 page: &Page<T>,
578 ) -> Result<Option<Page<T>>, Error> {
579 match page.next_url() {
580 None => Ok(None),
581 Some(next) if !is_same_origin(next, &self.shared.base_url) => Err(Error::usage(
582 format!("pagination Link header points to a different origin: {next}"),
583 )),
584 Some(next) => {
585 let mut operation = Operation::at(Method::GET, next.clone());
586 operation.info(page.info().clone());
587 operation.route = page.route();
588 self.send_page(operation).await.map(Some)
589 }
590 }
591 }
592
593 pub async fn each_page<T: DeserializeOwned>(
598 &self,
599 first: Page<T>,
600 mut visit: impl FnMut(&Page<T>) -> bool,
601 ) -> Result<(), Error> {
602 self.within_limit(Box::pin(async move {
603 let mut page = first;
604 let mut count = 1;
605 while visit(&page) {
606 if !page.has_next() {
607 break;
608 }
609 if count >= self.shared.max_pages {
610 return Err(Error::pagination_capped(self.shared.max_pages));
611 }
612 match self.next_page(&page).await? {
613 Some(next) => page = next,
614 None => break,
615 }
616 count += 1;
617 }
618 Ok(())
619 }))
620 .await
621 }
622
623 pub async fn execute(&self, operation: Operation) -> Result<Response, Error> {
628 let deadline = self.deadline();
629 let span = span_for(&operation);
630 span.wrap(self.instrument(&operation, deadline, self.dispatch(&operation, &span)))
631 .await
632 }
633
634 pub(crate) async fn stream(
641 &self,
642 operation: Operation,
643 deadline: Option<Instant>,
644 ) -> Result<HttpResponse<Body>, Error> {
645 let span = span_for(&operation);
646 span.wrap(self.instrument(&operation, deadline, self.streamed(&operation, &span)))
647 .await
648 }
649
650 pub(crate) fn deadline(&self) -> Option<Instant> {
654 match DEADLINE.try_with(|deadline| *deadline) {
655 Ok(inherited) => inherited,
656 Err(_) => self
657 .shared
658 .operation_timeout
659 .and_then(|limit| Instant::now().checked_add(limit)),
660 }
661 }
662
663 pub(crate) async fn within_limit<T>(
667 &self,
668 work: impl Future<Output = Result<T, Error>>,
669 ) -> Result<T, Error> {
670 let deadline = self.deadline();
671 DEADLINE
672 .scope(deadline, self.within_deadline(deadline, work))
673 .await
674 }
675
676 pub(crate) async fn within_deadline<T>(
681 &self,
682 deadline: Option<Instant>,
683 work: impl Future<Output = Result<T, Error>>,
684 ) -> Result<T, Error> {
685 match (deadline, self.shared.operation_timeout) {
686 (Some(deadline), Some(limit)) => {
687 match tokio::time::timeout_at(deadline.into(), work).await {
688 Ok(outcome) => outcome,
689 Err(_) => Err(Error::timed_out(limit)),
690 }
691 }
692 _ => work.await,
693 }
694 }
695
696 pub(crate) async fn as_operation<T>(
709 &self,
710 info: &OperationInfo,
711 work: impl Future<Output = Result<T, Error>>,
712 ) -> Result<T, Error> {
713 let deadline = self.deadline();
714 let span = OperationSpan::announced(info);
715 span.wrap(ENCLOSING.scope(
716 span.clone(),
717 DEADLINE.scope(deadline, self.announced(info, deadline, work)),
718 ))
719 .await
720 }
721
722 async fn instrument<T>(
727 &self,
728 operation: &Operation,
729 deadline: Option<Instant>,
730 work: impl Future<Output = Result<T, Error>>,
731 ) -> Result<T, Error> {
732 if operation.quiet {
733 self.within_deadline(deadline, work).await
734 } else {
735 self.announced(&operation.info, deadline, work).await
736 }
737 }
738
739 async fn announced<T>(
750 &self,
751 info: &OperationInfo,
752 deadline: Option<Instant>,
753 work: impl Future<Output = Result<T, Error>>,
754 ) -> Result<T, Error> {
755 let hooks = &self.shared.hooks;
756 self.within_deadline(deadline, hooks.on_operation_gate(info))
757 .await?;
758
759 let mut running = Running {
760 hooks,
761 info,
762 state: Some(hooks.on_operation_start(info)),
763 started: Instant::now(),
764 };
765 let outcome = self.within_deadline(deadline, work).await;
766 running.finished(outcome.as_ref().map(|_| ()));
767 outcome
768 }
769
770 async fn dispatch(
773 &self,
774 operation: &Operation,
775 span: &OperationSpan,
776 ) -> Result<Response, Error> {
777 let url = self.url_for(operation)?;
778 let mut answered = self.attempt(operation, &url).await?;
779 let status = answered.response.status();
780 span.answered(status, request_id(answered.response.headers()));
781 let finished = self
782 .finish(
783 operation,
784 &url,
785 answered.url,
786 answered.response,
787 answered.cached,
788 )
789 .await;
790 answered.sending.end(&RequestResult {
791 status: Some(status),
792 duration: answered.duration,
793 error: finished.as_ref().err(),
794 from_cache: finished.as_ref().is_ok_and(|response| response.from_cache),
795 retryable: answered.retryable,
796 retry_after: answered.retry_after,
797 });
798 finished
799 }
800
801 async fn streamed(
803 &self,
804 operation: &Operation,
805 span: &OperationSpan,
806 ) -> Result<HttpResponse<Body>, Error> {
807 let url = self.url_for(operation)?;
808 let mut answered = self.attempt(operation, &url).await?;
809 let status = answered.response.status();
810 span.answered(status, request_id(answered.response.headers()));
811 let failure = (!status.is_success()).then(|| {
812 Error::from_response(status, &operation.method, answered.response.headers(), &[])
813 });
814 answered.sending.end(&RequestResult {
815 status: Some(status),
816 duration: answered.duration,
817 error: failure.as_ref(),
818 from_cache: false,
819 retryable: answered.retryable,
820 retry_after: answered.retry_after,
821 });
822 match failure {
823 Some(error) => Err(error),
824 None => Ok(answered.response),
825 }
826 }
827
828 fn budget(&self, operation: &Operation) -> Budget {
837 let shared = &self.shared;
838 let ceiling = shared.max_retries.saturating_add(1);
839 let (attempts, retry_on, delay) = match operation.route.map(|route| &route.retry) {
840 Some(policy) if policy.max > 0 => (
841 policy.max.min(ceiling),
842 policy.retry_on,
843 Duration::from_millis(policy.base_delay_ms)
844 .max(shared.base_delay.unwrap_or(Duration::ZERO)),
845 ),
846 Some(_) => (1, &[][..], DEFAULT_BASE_DELAY),
847 None => (
848 ceiling,
849 RETRYABLE_STATUSES,
850 shared.base_delay.unwrap_or(DEFAULT_BASE_DELAY),
851 ),
852 };
853 Budget {
854 attempts: if operation.idempotent { attempts } else { 1 },
855 retry_on,
856 delay: delay.min(shared.max_delay),
857 }
858 }
859
860 #[allow(clippy::too_many_lines)] async fn attempt(&self, operation: &Operation, url: &Url) -> Result<Answered, Error> {
864 let hooks = &self.shared.hooks;
865 let budget = self.budget(operation);
866 let mut attempts = budget.attempts;
867 let mut attempt = 1;
868 let mut delay = budget.delay;
869 let mut refreshed = false;
870 let mut cached = None;
873
874 loop {
875 let (request, signed) = {
878 let _no_refresh = self.shared.refreshing.read().await;
879 self.prepare(operation, url, &mut cached).await?
880 };
881 let mut sending = Sending::start(
882 hooks.clone(),
883 RequestInfo {
884 method: operation.method.clone(),
885 url: url.clone(),
886 attempt,
887 },
888 );
889 let started = Instant::now();
890 let sent = {
892 let span = AttemptSpan::new(attempt);
893 let sent = span
894 .wrap(self.transmit(operation, url.clone(), request))
895 .await;
896 if let Ok(received) = &sent {
897 span.answered(received.response.status());
898 }
899 sent
900 };
901 let duration = started.elapsed();
902
903 match sent {
904 Err(error) => {
905 sending.end(&RequestResult {
906 status: None,
907 duration,
908 error: Some(&error),
909 from_cache: false,
910 retryable: true,
911 retry_after: None,
912 });
913 if attempt < attempts {
914 crate::trace::debug!(operation = label(operation), attempt, error = %error.code(), "request failed, retrying");
915 hooks.on_retry(&sending.info, attempt + 1, &error);
916 self.wait(delay).await;
917 delay = self.next_delay(delay);
918 attempt += 1;
919 } else {
920 return Err(error);
921 }
922 }
923 Ok(received) => {
924 let status = received.response.status();
925 let retryable = budget.retry_on.contains(&status.as_u16());
926 let retry_after = retry_after_asked(retryable, received.response.headers());
927 if status == StatusCode::UNAUTHORIZED
930 && received.authenticated
931 && !refreshed
932 && self
933 .refresh_credentials(signed.under, signed.bearer.clone())
934 .await
935 {
936 let cause = Error::auth("Token refreshed").retryable();
937 sending.end(&RequestResult {
938 status: Some(status),
939 duration,
940 error: Some(&cause),
941 from_cache: false,
942 retryable,
943 retry_after,
944 });
945 crate::trace::debug!(
946 operation = label(operation),
947 "credentials refreshed, resending"
948 );
949 hooks.on_retry(&sending.info, attempt + 1, &cause);
950 refreshed = true;
951 attempt += 1;
952 attempts = attempts.max(attempt);
953 } else if retryable && attempt < attempts {
954 let cause = Error::from_response(
955 status,
956 &operation.method,
957 received.response.headers(),
958 &[],
959 );
960 sending.end(&RequestResult {
961 status: Some(status),
962 duration,
963 error: Some(&cause),
964 from_cache: false,
965 retryable,
966 retry_after,
967 });
968 crate::trace::debug!(operation = label(operation), attempt, %status, "retryable status, retrying");
969 hooks.on_retry(&sending.info, attempt + 1, &cause);
970 match retry_after {
975 Some(seconds) if seconds > 0 => {
976 self.wait_as_asked(Duration::from_secs(seconds)).await;
977 }
978 _ => self.wait(delay).await,
979 }
980 delay = self.next_delay(delay);
981 attempt += 1;
982 } else {
983 let cached = if received.redirected {
987 None
988 } else {
989 cached.take()
990 };
991 return Ok(Answered {
992 url: received.url,
993 response: received.response,
994 cached,
995 sending,
996 duration,
997 retryable,
998 retry_after,
999 });
1000 }
1001 }
1002 }
1003 }
1004 }
1005
1006 async fn refresh_credentials(
1037 &self,
1038 signed_under: Generation,
1039 rejected: Option<HeaderValue>,
1040 ) -> bool {
1041 let shared = self.shared.clone();
1042 let interest = Interest::new();
1043 let wanted = interest.wanted.clone();
1044 let refresh = tokio::spawn(async move {
1045 let _turn = shared.refreshing.write().await;
1053 if shared.refreshes.load(Ordering::Acquire) != signed_under.refreshes {
1054 true
1057 } else if shared.refresh_runs.load(Ordering::Acquire) != signed_under.runs
1058 || !wanted.load(Ordering::Acquire)
1059 {
1060 false
1064 } else {
1065 let renewed = match rejected {
1066 Some(rejected) => match shared.bearer_now().await {
1067 Ok(Some(now)) if now != rejected => true,
1070 Ok(_) => shared.auth.refresh().await,
1071 Err(_) => false,
1075 },
1076 None => shared.auth.refresh().await,
1077 };
1078 if renewed {
1079 shared.refreshes.fetch_add(1, Ordering::AcqRel);
1080 *shared.signing.lock().await = None;
1084 }
1085 shared.refresh_runs.fetch_add(1, Ordering::AcqRel);
1086 renewed
1087 }
1088 });
1089 let refreshed = refresh.await.unwrap_or(false);
1090 drop(interest);
1091 refreshed
1092 }
1093
1094 pub(crate) fn url_for(&self, operation: &Operation) -> Result<Url, Error> {
1095 let mut url = if let Some(url) = &operation.url {
1096 url.clone()
1097 } else {
1098 let mut path = operation.path.clone();
1099 if operation.json_suffix {
1100 path = with_json_extension(&path);
1101 }
1102 self.shared.base_url.join(path.trim_start_matches('/'))?
1103 };
1104 if !operation.query.is_empty() {
1105 url.query_pairs_mut().extend_pairs(&operation.query);
1106 }
1107 if let Some(account_id) = self.account_id
1108 && is_same_origin(&url, &self.shared.base_url)
1109 {
1110 let others: Vec<(String, String)> = url
1111 .query_pairs()
1112 .filter(|(name, _)| name != ACCOUNT_FILTER_PARAMETER)
1113 .map(|(name, value)| (name.into_owned(), value.into_owned()))
1114 .collect();
1115 url.query_pairs_mut()
1116 .clear()
1117 .extend_pairs(others)
1118 .append_pair(ACCOUNT_FILTER_PARAMETER, &account_id.to_string());
1119 }
1120 Ok(url)
1121 }
1122
1123 async fn prepare(
1129 &self,
1130 operation: &Operation,
1131 url: &Url,
1132 cached: &mut Option<(String, CachedResponse)>,
1133 ) -> Result<(Request<Bytes>, Signed), Error> {
1134 let mut request = Request::builder()
1135 .method(operation.method.clone())
1136 .uri(url.as_str())
1137 .body(Bytes::new())
1138 .map_err(Error::from_std)?;
1139 let headers = request.headers_mut();
1140 headers.insert(USER_AGENT, header_value(&self.shared.user_agent)?);
1141 headers.insert(ACCEPT, HeaderValue::from_static(operation.accept));
1142 if let Some(body) = &operation.body {
1143 headers.insert(CONTENT_TYPE, header_value(&body.content_type)?);
1144 *request.body_mut() = body.bytes.clone();
1145 }
1146 let signed = self.sign(&mut request).await?;
1147
1148 let key = match self.cacheable(operation) {
1149 None => None,
1150 Some(cache) => match request
1151 .headers()
1152 .get(AUTHORIZATION)
1153 .and_then(|value| value.to_str().ok())
1154 {
1155 None => None,
1156 Some(credential) => {
1157 let key = cache_key(url.as_str(), credential);
1158 if cached.as_ref().is_none_or(|(held, _)| *held != key) {
1159 *cached = self.look_up(cache, &key).await;
1160 }
1161 Some(key)
1162 }
1163 },
1164 };
1165 if key.is_none() {
1168 *cached = None;
1169 }
1170 if let Some((_, entry)) = cached.as_ref()
1171 && !entry.etag.is_empty()
1172 {
1173 let validator = header_value(&entry.etag)?;
1174 request.headers_mut().insert(IF_NONE_MATCH, validator);
1175 }
1176 Ok((request, signed))
1177 }
1178
1179 async fn sign(&self, request: &mut Request<Bytes>) -> Result<Signed, Error> {
1187 let mut last = self.shared.signing.lock().await;
1188 self.shared.auth.authenticate(request).await?;
1189 let bearer = if self.shared.bearer_auth {
1190 request.headers().get(AUTHORIZATION).cloned()
1191 } else {
1192 None
1193 };
1194 if let Some(now) = &bearer {
1195 if last.as_ref().is_some_and(|before| before != now) {
1196 self.shared.refreshes.fetch_add(1, Ordering::AcqRel);
1198 self.shared.refresh_runs.fetch_add(1, Ordering::AcqRel);
1199 }
1200 *last = Some(now.clone());
1201 }
1202 Ok(Signed {
1203 under: self.shared.generation(),
1204 bearer,
1205 })
1206 }
1207
1208 async fn look_up(
1212 &self,
1213 cache: &Arc<dyn ResponseCache>,
1214 key: &str,
1215 ) -> Option<(String, CachedResponse)> {
1216 match cache_get(cache, key).await {
1217 Some(entry) if entry.body.len() <= self.shared.max_response_body_bytes => {
1218 Some((key.to_string(), entry))
1219 }
1220 Some(_) => {
1221 cache_invalidate(cache, key).await;
1222 None
1223 }
1224 None => Some((
1225 key.to_string(),
1226 CachedResponse {
1227 etag: String::new(),
1228 body: Bytes::new(),
1229 },
1230 )),
1231 }
1232 }
1233
1234 fn cacheable(&self, operation: &Operation) -> Option<&Arc<dyn ResponseCache>> {
1239 if !operation.no_cache
1240 && operation.method == Method::GET
1241 && operation.accept == "application/json"
1242 {
1243 self.shared.cache.as_ref()
1244 } else {
1245 None
1246 }
1247 }
1248
1249 async fn transmit(
1260 &self,
1261 operation: &Operation,
1262 mut url: Url,
1263 mut request: Request<Bytes>,
1264 ) -> Result<Received, Error> {
1265 let mut hops = 0;
1266 let mut authenticated = true;
1267 loop {
1268 let outgoing = (
1269 request.method().clone(),
1270 request.headers().clone(),
1271 request.body().clone(),
1272 );
1273 let response = self.shared.http.send(request).await?;
1274 let next = if operation.capture_redirects {
1275 None
1276 } else {
1277 redirect_target(&url, &response)
1278 };
1279 match next {
1280 None => {
1281 return Ok(Received {
1282 url,
1283 response,
1284 redirected: hops > 0,
1285 authenticated,
1286 });
1287 }
1288 Some(_) if hops == MAX_REDIRECTS => {
1289 return Err(Error::new(
1290 ErrorCode::Network,
1291 format!(
1292 "{} redirected more than {MAX_REDIRECTS} times",
1293 operation.label()
1294 ),
1295 )
1296 .retryable());
1297 }
1298 Some(next) => {
1299 require_secure_endpoint(&next)?;
1300 if !is_same_origin(&next, &url) {
1303 authenticated = false;
1304 }
1305 request = redirected(outgoing, response.status(), &url, &next)?;
1306 url = next;
1307 hops += 1;
1308 }
1309 }
1310 }
1311 }
1312
1313 fn buffer_bound(&self, operation: &Operation) -> usize {
1320 if is_parsed(operation.accept) {
1321 self.shared.max_response_body_bytes
1322 } else {
1323 MAX_RESPONSE_BODY_BYTES
1324 }
1325 }
1326
1327 async fn finish(
1328 &self,
1329 operation: &Operation,
1330 url: &Url,
1331 final_url: Url,
1332 response: HttpResponse<Body>,
1333 cached: Option<(String, CachedResponse)>,
1334 ) -> Result<Response, Error> {
1335 let status = response.status();
1336 let headers = response.headers().clone();
1337
1338 if status == StatusCode::NOT_MODIFIED {
1339 return match cached {
1340 Some((_, entry)) if !entry.etag.is_empty() => Ok(Response {
1341 status: StatusCode::OK,
1342 headers,
1343 body: entry.body,
1344 url: final_url,
1345 from_cache: true,
1346 empty: false,
1347 }),
1348 _ => Err(Error::api(
1349 304,
1350 "304 received but no cached response available",
1351 )),
1352 };
1353 }
1354
1355 let bound = self.buffer_bound(operation);
1356 let body = match read_body(response.into_body(), bound, &operation.method, url.path()).await
1357 {
1358 Ok(body) => body,
1359 Err(refusal) if status.is_success() => return Err(refusal),
1360 Err(refusal) => {
1363 return Err(
1364 Error::from_response(status, &operation.method, &headers, &[])
1365 .refusing(refusal),
1366 );
1367 }
1368 };
1369
1370 if status.is_success() {
1371 if let (Some((key, _)), Some(cache)) = (cached, self.cacheable(operation))
1372 && let Some(etag) = headers.get("etag").and_then(|value| value.to_str().ok())
1373 {
1374 cache_set(
1375 cache,
1376 &key,
1377 CachedResponse {
1378 etag: etag.to_string(),
1379 body: body.clone(),
1380 },
1381 )
1382 .await;
1383 }
1384 Ok(Response {
1385 status,
1386 headers,
1387 body,
1388 url: final_url,
1389 from_cache: false,
1390 empty: false,
1391 })
1392 } else if operation.empty_on.contains(&status.as_u16()) {
1393 Ok(Response {
1394 status,
1395 headers,
1396 body,
1397 url: final_url,
1398 from_cache: false,
1399 empty: true,
1400 })
1401 } else {
1402 Err(Error::from_response(
1403 status,
1404 &operation.method,
1405 &headers,
1406 &body,
1407 ))
1408 }
1409 }
1410
1411 async fn wait(&self, delay: Duration) {
1414 tokio::time::sleep((delay + self.jitter()).min(self.shared.max_delay)).await;
1415 }
1416
1417 async fn wait_as_asked(&self, delay: Duration) {
1421 tokio::time::sleep(delay + self.jitter()).await;
1422 }
1423
1424 fn jitter(&self) -> Duration {
1425 match self.shared.max_jitter.as_millis() {
1426 0 => Duration::ZERO,
1427 millis => Duration::from_millis(rand::random_range(
1428 0..u64::try_from(millis).unwrap_or(u64::MAX),
1429 )),
1430 }
1431 }
1432
1433 fn next_delay(&self, delay: Duration) -> Duration {
1434 (delay * 2).min(self.shared.max_delay)
1435 }
1436}
1437
1438struct Running<'a> {
1442 hooks: &'a Arc<dyn Hooks>,
1443 info: &'a OperationInfo,
1444 state: Option<OperationState>,
1445 started: Instant,
1446}
1447
1448impl Running<'_> {
1449 fn finished(&mut self, outcome: Result<(), &Error>) {
1450 if let Some(state) = self.state.take() {
1451 self.hooks
1452 .on_operation_end(self.info, state, outcome, self.started.elapsed());
1453 }
1454 }
1455}
1456
1457impl Drop for Running<'_> {
1458 fn drop(&mut self) {
1459 if self.state.is_some() {
1462 self.finished(Err(&Error::cancelled()));
1463 }
1464 }
1465}
1466
1467struct Budget {
1470 attempts: u32,
1471 retry_on: &'static [u16],
1472 delay: Duration,
1473}
1474
1475#[derive(Clone, Copy)]
1480struct Generation {
1481 refreshes: u64,
1482 runs: u64,
1483}
1484
1485struct Signed {
1489 under: Generation,
1490 bearer: Option<HeaderValue>,
1491}
1492
1493struct Interest {
1497 wanted: Arc<AtomicBool>,
1498}
1499
1500impl Interest {
1501 fn new() -> Interest {
1502 Interest {
1503 wanted: Arc::new(AtomicBool::new(true)),
1504 }
1505 }
1506}
1507
1508impl Drop for Interest {
1509 fn drop(&mut self) {
1510 self.wanted.store(false, Ordering::Release);
1511 }
1512}
1513
1514struct Sending {
1520 hooks: Arc<dyn Hooks>,
1521 info: RequestInfo,
1522 started: Instant,
1523 owed: bool,
1524}
1525
1526impl Sending {
1527 fn start(hooks: Arc<dyn Hooks>, info: RequestInfo) -> Sending {
1528 hooks.on_request_start(&info);
1529 Sending {
1530 hooks,
1531 info,
1532 started: Instant::now(),
1533 owed: true,
1534 }
1535 }
1536
1537 fn end(&mut self, result: &RequestResult<'_>) {
1538 self.owed = false;
1539 self.hooks.on_request_end(&self.info, result);
1540 }
1541}
1542
1543impl Drop for Sending {
1544 fn drop(&mut self) {
1545 if self.owed {
1546 self.end(&RequestResult {
1547 status: None,
1548 duration: self.started.elapsed(),
1549 error: Some(&Error::cancelled()),
1550 from_cache: false,
1551 retryable: false,
1552 retry_after: None,
1553 });
1554 }
1555 }
1556}
1557
1558struct Received {
1563 url: Url,
1564 response: HttpResponse<Body>,
1565 redirected: bool,
1566 authenticated: bool,
1567}
1568
1569struct Answered {
1573 url: Url,
1574 response: HttpResponse<Body>,
1575 cached: Option<(String, CachedResponse)>,
1576 sending: Sending,
1577 duration: Duration,
1578 retryable: bool,
1579 retry_after: Option<u64>,
1580}
1581
1582async fn cache_get(cache: &Arc<dyn ResponseCache>, key: &str) -> Option<CachedResponse> {
1588 let cache = cache.clone();
1589 let key = key.to_string();
1590 tokio::task::spawn_blocking(move || cache.get(&key))
1591 .await
1592 .ok()
1593 .flatten()
1594}
1595
1596async fn cache_set(cache: &Arc<dyn ResponseCache>, key: &str, response: CachedResponse) {
1597 let cache = cache.clone();
1598 let key = key.to_string();
1599 let _ = tokio::task::spawn_blocking(move || cache.set(&key, response)).await;
1600}
1601
1602async fn cache_invalidate(cache: &Arc<dyn ResponseCache>, key: &str) {
1603 let cache = cache.clone();
1604 let key = key.to_string();
1605 let _ = tokio::task::spawn_blocking(move || cache.invalidate(&key)).await;
1606}
1607
1608#[cfg(feature = "reqwest")]
1609fn shipped_http_client(timeout: Duration) -> Result<Arc<dyn HttpClient>, Error> {
1610 Ok(Arc::new(crate::http::ReqwestClient::with_timeout(timeout)?))
1611}
1612
1613#[cfg(not(feature = "reqwest"))]
1614fn shipped_http_client(_timeout: Duration) -> Result<Arc<dyn HttpClient>, Error> {
1615 Err(Error::usage(
1616 "no HTTP client: supply one with ClientBuilder::http_client, or enable the reqwest feature",
1617 ))
1618}
1619
1620fn redirect_target(url: &Url, response: &HttpResponse<Body>) -> Option<Url> {
1623 let status = response.status();
1624 if status.is_redirection() && status != StatusCode::NOT_MODIFIED {
1625 response
1626 .headers()
1627 .get("location")
1628 .and_then(|value| value.to_str().ok())
1629 .and_then(|location| url.join(location).ok())
1630 } else {
1631 None
1632 }
1633}
1634
1635fn redirected(
1639 (method, mut headers, body): (Method, HeaderMap, Bytes),
1640 status: StatusCode,
1641 from: &Url,
1642 next: &Url,
1643) -> Result<Request<Bytes>, Error> {
1644 let (method, body) = if keeps_method(&method, status) {
1645 (method, body)
1646 } else {
1647 headers.remove(CONTENT_TYPE);
1648 headers.remove(CONTENT_LENGTH);
1649 (Method::GET, Bytes::new())
1650 };
1651 headers.remove(IF_NONE_MATCH);
1653 if !is_same_origin(next, from) {
1654 headers.remove(AUTHORIZATION);
1655 headers.remove(COOKIE);
1656 headers.remove(PROXY_AUTHORIZATION);
1657 }
1658 let mut request = Request::builder()
1659 .method(method)
1660 .uri(next.as_str())
1661 .body(body)
1662 .map_err(Error::from_std)?;
1663 *request.headers_mut() = headers;
1664 Ok(request)
1665}
1666
1667fn keeps_method(method: &Method, status: StatusCode) -> bool {
1673 match status {
1674 StatusCode::SEE_OTHER => method == Method::GET || method == Method::HEAD,
1675 StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND => method != Method::POST,
1676 _ => true,
1677 }
1678}
1679
1680fn parse_base_url(base_url: &str) -> Result<Url, Error> {
1681 let mut url = Url::parse(base_url)
1682 .map_err(|error| Error::usage(format!("base URL {base_url}: {error}")))?;
1683 require_secure_endpoint(&url)?;
1684 if !url.path().ends_with('/') {
1685 url.set_path(&format!("{}/", url.path()));
1686 }
1687 Ok(url)
1688}
1689
1690pub(crate) fn with_json_extension(path: &str) -> String {
1694 let last_segment = path.rsplit('/').next().unwrap_or_default();
1695 if path.is_empty() || path.ends_with('/') || last_segment.contains('.') {
1696 path.to_string()
1697 } else {
1698 format!("{path}.json")
1699 }
1700}
1701
1702fn span_for(operation: &Operation) -> OperationSpan {
1706 if operation.quiet {
1707 ENCLOSING
1708 .try_with(Clone::clone)
1709 .unwrap_or_else(|_| OperationSpan::none())
1710 } else {
1711 OperationSpan::new(operation)
1712 }
1713}
1714
1715fn request_id(headers: &HeaderMap) -> Option<&str> {
1717 headers
1718 .get("x-request-id")
1719 .and_then(|value| value.to_str().ok())
1720}
1721
1722fn retry_after_asked(retryable: bool, headers: &HeaderMap) -> Option<u64> {
1725 if retryable {
1726 retry_after_seconds(headers)
1727 } else {
1728 None
1729 }
1730}
1731
1732fn header_value(value: &str) -> Result<HeaderValue, Error> {
1733 HeaderValue::from_str(value)
1734 .map_err(|_| Error::usage(format!("{value:?} is not a valid header value")))
1735}
1736
1737fn is_parsed(accept: &str) -> bool {
1741 accept.is_empty()
1742 || accept.split(',').any(|part| {
1743 let media_type = part.split(';').next().unwrap_or_default().trim();
1744 media_type == "application/json"
1745 || media_type.ends_with("+json")
1746 || media_type == "text/html"
1747 })
1748}
1749
1750pub(crate) async fn read_body(
1753 body: Body,
1754 limit: usize,
1755 method: &Method,
1756 path: &str,
1757) -> Result<Bytes, Error> {
1758 body.collect(limit, || Error::response_too_large(limit, method, path))
1759 .await
1760}
1761
1762#[cfg(test)]
1763mod tests {
1764 use std::sync::Mutex;
1765 use std::sync::atomic::AtomicUsize;
1766
1767 use async_trait::async_trait;
1768 use serde_json::Value;
1769
1770 use super::*;
1771 use crate::auth::StaticTokenProvider;
1772 use crate::cache::InMemoryCache;
1773
1774 struct Canned {
1778 answer: Box<Answer>,
1779 sent: Mutex<Vec<(Method, String, HeaderMap, Bytes)>>,
1780 }
1781
1782 type Answer = dyn Fn(&Request<Bytes>) -> HttpResponse<Body> + Send + Sync;
1783
1784 impl Canned {
1785 fn new(
1786 answer: impl Fn(&Request<Bytes>) -> HttpResponse<Body> + Send + Sync + 'static,
1787 ) -> Arc<Canned> {
1788 Arc::new(Canned {
1789 answer: Box::new(answer),
1790 sent: Mutex::new(Vec::new()),
1791 })
1792 }
1793
1794 fn sent(&self) -> Vec<(Method, String, HeaderMap, Bytes)> {
1795 self.sent.lock().unwrap().clone()
1796 }
1797 }
1798
1799 #[async_trait]
1800 impl HttpClient for Arc<Canned> {
1801 async fn send(&self, request: Request<Bytes>) -> Result<HttpResponse<Body>, Error> {
1802 self.sent.lock().unwrap().push((
1803 request.method().clone(),
1804 request.uri().to_string(),
1805 request.headers().clone(),
1806 request.body().clone(),
1807 ));
1808 Ok((self.answer)(&request))
1809 }
1810 }
1811
1812 fn answer(status: u16, body: &'static str) -> HttpResponse<Body> {
1813 let mut response = HttpResponse::new(Body::from(body));
1814 *response.status_mut() = StatusCode::from_u16(status).unwrap();
1815 response
1816 }
1817
1818 fn redirect(location: &str) -> HttpResponse<Body> {
1819 redirect_with(302, location)
1820 }
1821
1822 fn redirect_with(status: u16, location: &str) -> HttpResponse<Body> {
1823 let mut response = answer(status, "");
1824 response
1825 .headers_mut()
1826 .insert("location", HeaderValue::from_str(location).unwrap());
1827 response
1828 }
1829
1830 fn tagged(body: &'static str, etag: &str) -> HttpResponse<Body> {
1831 let mut response = answer(200, body);
1832 response
1833 .headers_mut()
1834 .insert("etag", HeaderValue::from_str(etag).unwrap());
1835 response
1836 }
1837
1838 fn not_modified(etag: &str) -> HttpResponse<Body> {
1839 let mut response = answer(304, "");
1840 response
1841 .headers_mut()
1842 .insert("etag", HeaderValue::from_str(etag).unwrap());
1843 response
1844 }
1845
1846 fn client_over(http: Arc<Canned>) -> Client {
1847 client_with(http, StaticTokenProvider::new("secret"))
1848 }
1849
1850 fn client_with(http: Arc<Canned>, provider: impl TokenProvider + 'static) -> Client {
1851 Client::builder(Config::default().with_base_url("https://hey.test"))
1852 .token_provider(provider)
1853 .http_client(http)
1854 .max_retries(0)
1855 .build()
1856 .unwrap()
1857 }
1858
1859 fn caching_client_over(http: Arc<Canned>) -> Client {
1860 Client::builder(Config::default().with_base_url("https://hey.test"))
1861 .token_provider(StaticTokenProvider::new("secret"))
1862 .http_client(http)
1863 .cache(InMemoryCache::new())
1864 .max_retries(0)
1865 .build()
1866 .unwrap()
1867 }
1868
1869 struct Renewing {
1871 token: Mutex<String>,
1872 refreshes: AtomicUsize,
1873 }
1874
1875 impl Renewing {
1876 fn new() -> Arc<Renewing> {
1877 Arc::new(Renewing {
1878 token: Mutex::new("stale".to_string()),
1879 refreshes: AtomicUsize::new(0),
1880 })
1881 }
1882
1883 fn refreshes(&self) -> usize {
1884 self.refreshes.load(Ordering::SeqCst)
1885 }
1886 }
1887
1888 #[async_trait]
1889 impl TokenProvider for Renewing {
1890 async fn access_token(&self) -> Result<String, Error> {
1891 Ok(self.token.lock().unwrap().clone())
1892 }
1893
1894 async fn refresh(&self) -> bool {
1895 self.refreshes.fetch_add(1, Ordering::SeqCst);
1896 *self.token.lock().unwrap() = "fresh".to_string();
1897 true
1898 }
1899 }
1900
1901 #[tokio::test]
1902 async fn a_request_goes_out_on_the_supplied_http_client_with_credentials() {
1903 let http = Canned::new(|_| answer(200, r#"{"ok":true}"#));
1904 let client = client_over(http.clone());
1905
1906 let body: Value = client
1907 .send(client.request(Method::GET, "/boxes"))
1908 .await
1909 .unwrap();
1910
1911 assert_eq!(body, serde_json::json!({ "ok": true }));
1912 let sent = http.sent();
1913 assert_eq!(sent.len(), 1);
1914 assert_eq!(sent[0].0, Method::GET);
1915 assert_eq!(sent[0].1, "https://hey.test/boxes.json");
1916 assert_eq!(sent[0].2[AUTHORIZATION], "Bearer secret");
1917 }
1918
1919 #[tokio::test]
1920 async fn a_redirect_on_the_same_origin_is_followed_with_credentials() {
1921 let http = Canned::new(|request| {
1922 if request.uri().path() == "/old.json" {
1923 redirect("/new.json")
1924 } else {
1925 answer(200, r#"{"moved":true}"#)
1926 }
1927 });
1928 let client = client_over(http.clone());
1929
1930 let response = client
1931 .execute(client.request(Method::GET, "/old"))
1932 .await
1933 .unwrap();
1934
1935 assert_eq!(response.url.as_str(), "https://hey.test/new.json");
1936 assert_eq!(response.body, r#"{"moved":true}"#);
1937 let sent = http.sent();
1938 assert_eq!(sent.len(), 2);
1939 assert_eq!(sent[1].1, "https://hey.test/new.json");
1940 assert_eq!(sent[1].2[AUTHORIZATION], "Bearer secret");
1941 }
1942
1943 async fn hop_of(method: Method, status: u16) -> (Method, HeaderMap, Bytes) {
1946 let http = Canned::new(move |request| {
1947 if request.uri().path() == "/old.json" {
1948 redirect_with(status, "/new.json")
1949 } else {
1950 answer(200, "{}")
1951 }
1952 });
1953 let client = client_over(http.clone());
1954 let mut operation = client.request(method, "/old");
1955 operation
1956 .json(&serde_json::json!({ "name": "renamed" }))
1957 .unwrap();
1958
1959 client.execute(operation).await.unwrap();
1960
1961 let sent = http.sent();
1962 assert_eq!(sent.len(), 2);
1963 assert_eq!(sent[1].1, "https://hey.test/new.json");
1964 let (method, _, headers, body) = sent.into_iter().nth(1).unwrap();
1965 (method, headers, body)
1966 }
1967
1968 #[tokio::test]
1971 async fn a_302_keeps_a_put_and_its_body() {
1972 let (method, headers, body) = hop_of(Method::PUT, 302).await;
1973
1974 assert_eq!(method, Method::PUT);
1975 assert_eq!(body, r#"{"name":"renamed"}"#);
1976 assert_eq!(headers[CONTENT_TYPE], "application/json");
1977 }
1978
1979 #[tokio::test]
1980 async fn a_301_keeps_a_delete() {
1981 let (method, _, _) = hop_of(Method::DELETE, 301).await;
1982
1983 assert_eq!(method, Method::DELETE);
1984 }
1985
1986 #[tokio::test]
1987 async fn a_302_turns_a_post_into_a_get_without_its_body() {
1988 let (method, headers, body) = hop_of(Method::POST, 302).await;
1989
1990 assert_eq!(method, Method::GET);
1991 assert!(body.is_empty());
1992 assert!(headers.get(CONTENT_TYPE).is_none());
1993 assert!(headers.get(CONTENT_LENGTH).is_none());
1994 }
1995
1996 #[tokio::test]
1998 async fn a_303_turns_a_delete_into_a_get() {
1999 let (method, _, body) = hop_of(Method::DELETE, 303).await;
2000
2001 assert_eq!(method, Method::GET);
2002 assert!(body.is_empty());
2003 }
2004
2005 #[tokio::test]
2006 async fn a_307_keeps_a_patch_and_its_body() {
2007 let (method, _, body) = hop_of(Method::PATCH, 307).await;
2008
2009 assert_eq!(method, Method::PATCH);
2010 assert_eq!(body, r#"{"name":"renamed"}"#);
2011 }
2012
2013 #[tokio::test]
2014 async fn a_308_keeps_a_post_and_its_body() {
2015 let (method, _, body) = hop_of(Method::POST, 308).await;
2016
2017 assert_eq!(method, Method::POST);
2018 assert_eq!(body, r#"{"name":"renamed"}"#);
2019 }
2020
2021 #[tokio::test]
2022 async fn an_html_read_asks_for_the_page_as_hey_serves_it() {
2023 let http = Canned::new(|_| {
2024 answer(
2025 200,
2026 r#"<section id="container_workflow_stage_5512"></section>"#,
2027 )
2028 });
2029 let client = client_over(http.clone());
2030
2031 let page = client.workflows().get_stage(8801, 5512).await.unwrap();
2032
2033 assert_eq!(
2034 page,
2035 r#"<section id="container_workflow_stage_5512"></section>"#
2036 );
2037 let sent = http.sent();
2038 assert_eq!(sent.len(), 1);
2039 assert_eq!(sent[0].1, "https://hey.test/workflows/8801/stages/5512");
2040 assert_eq!(sent[0].2[ACCEPT], "text/html");
2041 assert_eq!(sent[0].2[AUTHORIZATION], "Bearer secret");
2042 }
2043
2044 #[tokio::test]
2045 async fn a_redirect_off_the_origin_is_followed_without_credentials() {
2046 let http = Canned::new(|request| {
2047 if request.uri().host() == Some("hey.test") {
2048 redirect("https://storage.test/blobs/1")
2049 } else {
2050 answer(200, "the bytes")
2051 }
2052 });
2053 let client = client_over(http.clone());
2054
2055 let response = client.get_blob("/blobs/1").await.unwrap();
2056
2057 assert_eq!(response.body, "the bytes");
2058 let sent = http.sent();
2059 assert_eq!(sent.len(), 2);
2060 assert_eq!(sent[1].1, "https://storage.test/blobs/1");
2061 assert!(sent[1].2.get(AUTHORIZATION).is_none());
2062 }
2063
2064 #[tokio::test]
2065 async fn a_redirect_to_plain_http_elsewhere_is_refused() {
2066 let http = Canned::new(|_| redirect("http://evil.test/"));
2067 let client = client_over(http.clone());
2068
2069 let error = client.get("/anything").await.unwrap_err();
2070
2071 assert_eq!(error.code(), ErrorCode::Usage);
2072 assert_eq!(http.sent().len(), 1);
2073 }
2074
2075 #[tokio::test]
2076 async fn a_redirect_loop_is_given_up_on() {
2077 let http = Canned::new(|_| redirect("/again"));
2078 let client = client_over(http.clone());
2079
2080 let error = client.get("/again").await.unwrap_err();
2081
2082 assert_eq!(error.code(), ErrorCode::Network);
2083 assert_eq!(http.sent().len(), MAX_REDIRECTS + 1);
2084 }
2085
2086 #[tokio::test]
2087 async fn a_form_request_keeps_its_redirect_rather_than_following_it() {
2088 let http = Canned::new(|_| redirect("/workflows/8801"));
2089 let client = client_over(http.clone());
2090
2091 let created = client
2092 .post_form("/workflows", &[("workflow[name]", "Launch")])
2093 .await
2094 .unwrap();
2095
2096 assert_eq!(created.location.as_deref(), Some("/workflows/8801"));
2097 assert_eq!(http.sent().len(), 1);
2098 }
2099
2100 #[tokio::test]
2101 async fn a_redirect_neither_carries_nor_takes_the_cache_entry_of_the_url_asked_for() {
2102 let reads = Mutex::new(0);
2105 let http = Canned::new(move |request| {
2106 if request.uri().path() == "/a.json" {
2107 let mut reads = reads.lock().unwrap();
2108 *reads += 1;
2109 match *reads {
2110 1 => tagged(r#"{"which":"a"}"#, "\"x\""),
2111 2 => redirect("/b.json"),
2112 _ => not_modified("\"x\""),
2113 }
2114 } else {
2115 tagged(r#"{"which":"b"}"#, "\"x\"")
2116 }
2117 });
2118 let client = caching_client_over(http.clone());
2119
2120 let first = client
2121 .execute(client.request(Method::GET, "/a"))
2122 .await
2123 .unwrap();
2124 let through = client
2125 .execute(client.request(Method::GET, "/a"))
2126 .await
2127 .unwrap();
2128 let again = client
2129 .execute(client.request(Method::GET, "/a"))
2130 .await
2131 .unwrap();
2132
2133 assert_eq!(first.body, r#"{"which":"a"}"#);
2134 assert_eq!(
2135 through.body, r#"{"which":"b"}"#,
2136 "the answer reached through the redirect is b's"
2137 );
2138 assert!(!through.from_cache);
2139 assert_eq!(
2140 again.body, r#"{"which":"a"}"#,
2141 "a's entry is still a's, not b's"
2142 );
2143 assert!(again.from_cache);
2144 let sent = http.sent();
2145 assert_eq!(sent.len(), 4);
2146 assert_eq!(sent[1].2[IF_NONE_MATCH], "\"x\"");
2147 assert_eq!(sent[2].1, "https://hey.test/b.json");
2148 assert!(
2149 sent[2].2.get(IF_NONE_MATCH).is_none(),
2150 "b is not asked to validate a's entry"
2151 );
2152 assert_eq!(sent[3].2[IF_NONE_MATCH], "\"x\"");
2153 }
2154
2155 #[tokio::test]
2156 async fn a_304_from_a_redirect_target_is_not_answered_from_the_cache() {
2157 let reads = Mutex::new(0);
2158 let http = Canned::new(move |request| {
2159 if request.uri().path() == "/a.json" {
2160 let mut reads = reads.lock().unwrap();
2161 *reads += 1;
2162 if *reads == 1 {
2163 tagged(r#"{"which":"a"}"#, "\"x\"")
2164 } else {
2165 redirect("/b.json")
2166 }
2167 } else {
2168 not_modified("\"x\"")
2169 }
2170 });
2171 let client = caching_client_over(http.clone());
2172
2173 client
2174 .execute(client.request(Method::GET, "/a"))
2175 .await
2176 .unwrap();
2177 let error = client
2178 .execute(client.request(Method::GET, "/a"))
2179 .await
2180 .unwrap_err();
2181
2182 assert_eq!(error.http_status(), Some(304));
2183 assert_eq!(http.sent().len(), 3);
2184 }
2185
2186 #[tokio::test]
2187 async fn a_401_from_a_hop_that_carried_no_credentials_refreshes_nothing() {
2188 let http = Canned::new(|request| {
2189 if request.uri().host() == Some("hey.test") {
2190 redirect("https://files.test/export.json")
2191 } else {
2192 answer(401, "")
2193 }
2194 });
2195 let provider = Renewing::new();
2196 let client = client_with(http.clone(), provider.clone());
2197
2198 let error = client
2199 .execute(client.request(Method::GET, "/boxes"))
2200 .await
2201 .unwrap_err();
2202
2203 assert_eq!(error.code(), ErrorCode::Auth);
2204 assert_eq!(error.http_status(), Some(401));
2205 assert_eq!(
2206 provider.refreshes(),
2207 0,
2208 "HEY's credentials were not the ones rejected"
2209 );
2210 assert_eq!(http.sent().len(), 2, "and nothing is sent again");
2211 }
2212
2213 #[tokio::test]
2214 async fn a_hop_back_to_the_origin_does_not_bring_the_credentials_with_it() {
2215 let http = Canned::new(|request| match request.uri().host() {
2216 Some("hey.test") if request.uri().path() == "/boxes.json" => {
2217 redirect("https://files.test/boxes")
2218 }
2219 Some("hey.test") => answer(401, ""),
2220 _ => redirect("https://hey.test/elsewhere.json"),
2221 });
2222 let provider = Renewing::new();
2223 let client = client_with(http.clone(), provider.clone());
2224
2225 let error = client
2226 .execute(client.request(Method::GET, "/boxes"))
2227 .await
2228 .unwrap_err();
2229
2230 assert_eq!(error.code(), ErrorCode::Auth);
2231 assert_eq!(provider.refreshes(), 0);
2232 let sent = http.sent();
2233 assert_eq!(sent.len(), 3);
2234 assert_eq!(sent[2].1, "https://hey.test/elsewhere.json");
2235 assert!(sent[2].2.get(AUTHORIZATION).is_none());
2236 }
2237
2238 #[tokio::test]
2239 async fn a_401_on_a_redirect_that_stayed_on_the_origin_is_still_refreshed() {
2240 let http = Canned::new(|request| {
2241 if request.uri().path() == "/old.json" {
2242 redirect("/new.json")
2243 } else if request
2244 .headers()
2245 .get(AUTHORIZATION)
2246 .is_some_and(|token| token == "Bearer fresh")
2247 {
2248 answer(200, r#"{"moved":true}"#)
2249 } else {
2250 answer(401, "")
2251 }
2252 });
2253 let provider = Renewing::new();
2254 let client = client_with(http.clone(), provider.clone());
2255
2256 let response = client
2257 .execute(client.request(Method::GET, "/old"))
2258 .await
2259 .unwrap();
2260
2261 assert_eq!(response.body, r#"{"moved":true}"#);
2262 assert_eq!(provider.refreshes(), 1);
2263 let sent = http.sent();
2264 assert_eq!(sent.len(), 4);
2265 assert_eq!(sent[1].1, "https://hey.test/new.json");
2266 assert_eq!(sent[1].2[AUTHORIZATION], "Bearer stale");
2267 assert_eq!(sent[3].1, "https://hey.test/new.json");
2268 assert_eq!(sent[3].2[AUTHORIZATION], "Bearer fresh");
2269 }
2270
2271 #[test]
2272 fn json_extension_is_added_only_where_missing() {
2273 assert_eq!(with_json_extension("/boxes/123"), "/boxes/123.json");
2274 assert_eq!(with_json_extension("/boxes.json"), "/boxes.json");
2275 assert_eq!(
2276 with_json_extension("/calendar/days/2026-03-04/journal_entry"),
2277 "/calendar/days/2026-03-04/journal_entry.json"
2278 );
2279 assert_eq!(
2280 with_json_extension("/rails/active_storage/direct_uploads.json"),
2281 "/rails/active_storage/direct_uploads.json"
2282 );
2283 assert_eq!(with_json_extension("/boxes/"), "/boxes/");
2284 }
2285
2286 #[test]
2287 fn base_url_must_be_https_or_local() {
2288 assert!(parse_base_url("https://app.hey.com").is_ok());
2289 assert!(parse_base_url("http://127.0.0.1:3000").is_ok());
2290 assert_eq!(
2291 parse_base_url("http://evil.example.com")
2292 .unwrap_err()
2293 .code(),
2294 crate::ErrorCode::Usage
2295 );
2296 }
2297}