Skip to main content

hey_sdk/
client.rs

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
35/// How long the HTTP client the SDK ships gives an answer to arrive.
36pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
37/// How many times an idempotent operation is resent after a transient failure.
38pub const DEFAULT_MAX_RETRIES: u32 = 3;
39/// The wait before the first resend; each one after doubles it.
40pub const DEFAULT_BASE_DELAY: Duration = Duration::from_secs(1);
41/// The longest the client waits between attempts, however many it has made.
42///
43/// This is a deliberate divergence from Go, whose backoff doubles without a ceiling: by the
44/// fourth attempt there it is already eight seconds, and a caller who raised
45/// [`ClientBuilder::max_retries`] would be waiting minutes on a scope that is never coming
46/// back. The circuit breaker is what should give up on that scope; the backoff's job is to
47/// stop hammering, and thirty seconds does it. Move it with [`ClientBuilder::max_delay`].
48pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(30);
49/// The most added at random to each wait, so resends from many clients do not land
50/// together.
51pub const DEFAULT_MAX_JITTER: Duration = Duration::from_millis(100);
52/// How many pages a walk reads before it stops.
53pub const DEFAULT_MAX_PAGES: usize = 10_000;
54/// The most a JSON or HTML answer may deliver before the client refuses to hold it.
55pub const DEFAULT_MAX_RESPONSE_BODY_BYTES: usize = 16 << 20;
56
57/// The most the client buffers of an answer the configurable cap leaves alone: a blob, an
58/// export, whatever a form request answered. Only [`Client::download_blob`], which writes
59/// to the caller's destination as the bytes arrive, reads without a bound.
60pub const MAX_RESPONSE_BODY_BYTES: usize = 50 << 20;
61
62/// The statuses a request the model says nothing about — a path the caller wrote — is
63/// resent on. A modelled route is resent on the statuses its own policy names.
64const RETRYABLE_STATUSES: &[u16] = &[429, 500, 502, 503, 504];
65const ACCOUNT_FILTER_PARAMETER: &str = "filtered_account_id";
66/// How many redirects one request may go through before the client gives up on it, which
67/// is what reqwest allowed when it was the one following them.
68const MAX_REDIRECTS: usize = 10;
69
70tokio::task_local! {
71    /// The deadline the operation in progress on this task is held to, so that every
72    /// request a convenience or a walk makes inside it is held to the same one rather
73    /// than each starting a limit of its own.
74    static DEADLINE: Option<Instant>;
75
76    /// The span of the operation a convenience is running its requests inside with
77    /// [`Client::as_operation`], so that a quiet send made there records its answer on
78    /// that span rather than on none.
79    static ENCLOSING: OperationSpan;
80}
81
82/// A HEY client: one authenticated identity, presenting mail from All Accounts unless
83/// derived for one linked account with [`Client::for_account`].
84///
85/// Clients are cheap to clone and share their connection pool, credentials and cache.
86#[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    /// Whether [`Shared::auth`] is the SDK's own bearer strategy over a token provider, as
99    /// [`ClientBuilder::token_provider`] builds it. Only then is the bearer a signing put on
100    /// read for a renewal. Known from how the client was built rather than from anything on
101    /// a request, since a strategy of the caller's may sign through [`BearerAuth`] and then
102    /// rewrite the header — a per-request signature, say — and would otherwise have every
103    /// signing taken for a renewal and its refresh never asked for.
104    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    /// How many times the credentials have been refreshed. A request remembers the count it
116    /// was signed under, so a 401 answered after someone else refreshed is resent on the
117    /// new credentials rather than refreshing again.
118    pub(crate) refreshes: AtomicU64,
119    /// How many refreshes have run to an answer, renewed or not. A request remembers this
120    /// count too, so a 401 on credentials a refresh already failed to renew shares that
121    /// failure rather than asking the token endpoint again for the same credentials.
122    pub(crate) refresh_runs: AtomicU64,
123    /// One refresh at a time, and none while a request is being signed: the 401s a stale
124    /// credential earns all arrive together, and only the first of them should cost a
125    /// round trip to the token endpoint. Signing takes this for reading, so a signing
126    /// never waits on a refresh's turn being queued behind it; a refresh takes it for
127    /// writing, so the counts a request is signed under are those of the credentials it
128    /// carries. Taken before [`Shared::signing`], always: a signing holds the read half
129    /// and then the mutex, a refresh holds the write half — which no signer holds a read
130    /// under — and then the mutex, so the two locks can never be waited for in the other
131    /// order.
132    pub(crate) refreshing: tokio::sync::RwLock<()>,
133    /// The bearer the SDK's own strategy last signed with, and the lock every signing runs
134    /// under: the token is taken from the provider, compared with this, and the counts read
135    /// in one critical section, so the order the counts record is the order the provider
136    /// issued in. Concurrent signers otherwise let a token issued first be recorded second
137    /// — a request that came out with the newer token reads the counts of the older, and a
138    /// genuine 401 on the newer is taken for one already answered and merely resent. A
139    /// token other than the one here is a renewal the provider made of its own accord, and
140    /// moves the counts as a refresh would. `None` until a signing, and again after a
141    /// refresh, whose renewal is counted once, by the refresh: the first signing after it
142    /// carries the new token and is not counted again. Only the SDK's own [`BearerAuth`]
143    /// is read this way, as [`Shared::bearer_auth`] says; a strategy of the caller's may sign
144    /// every request differently, so it is never compared, though its signings take the
145    /// lock too, which costs it nothing a single signer at a time does not.
146    pub(crate) signing: Mutex<Option<HeaderValue>>,
147}
148
149impl Shared {
150    /// The credentials a request is signed under, as the counts at its signing. Read under
151    /// [`Shared::refreshing`] and [`Shared::signing`], so no refresh and no other signing
152    /// moves either between the two.
153    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    /// What the SDK's own bearer strategy would sign with now, asked by signing a request
161    /// that goes nowhere: a refresh checks it against the bearer a 401 came back on before
162    /// spending the provider's refresh on a token it has already replaced. Asked only of
163    /// the SDK's own bearer strategy, so a strategy of the caller's is never asked to sign
164    /// for nothing. An error is the provider failing to hand over any token at all — often
165    /// its own renewal failing — and is the refresh's answer, not a reason to ask again.
166    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/// What a client works out about the identity it presents and keeps for as long as it
174/// lives. A client derived with [`Client::for_account`] starts an empty one of its own,
175/// since none of it means the same thing under another account.
176#[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/// What came back from HEY, before it is decoded.
184#[derive(Debug, Clone)]
185#[non_exhaustive]
186pub struct Response {
187    /// What HEY answered.
188    pub status: StatusCode,
189    /// The headers that came with it.
190    pub headers: HeaderMap,
191    /// The body, read whole.
192    pub body: Bytes,
193    /// Where the answer came from, once any redirects were followed.
194    pub url: Url,
195    /// The body came out of the response cache: HEY answered 304 and the SDK read the
196    /// entry it was holding.
197    pub from_cache: bool,
198    /// The operation takes this status for an answer rather than a failure: a 404 that
199    /// means "nothing there", or the redirect a form request went out to collect.
200    pub empty: bool,
201}
202
203impl Response {
204    /// Decodes the body as JSON. A body that will not decode is an error that still says
205    /// what HEY answered: the status, and the request id when the answer named one.
206    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    /// One header's value, when HEY sent it and it is text.
221    pub fn header(&self, name: &str) -> Option<&str> {
222        self.headers.get(name).and_then(|value| value.to_str().ok())
223    }
224}
225
226/// How a [`Client`] is put together: credentials, the HTTP client, the retry budget, the
227/// cache and the hooks, each with a default a caller can move.
228pub 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    /// A builder for `config`, at the defaults and without credentials.
248    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    /// Authenticates with a bearer token drawn from `provider` for each request.
269    #[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    /// Authenticates however `strategy` does: the way in for anything but a bearer token.
277    #[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    /// Replaces the HTTP client every request goes out on, including the attachment bytes
285    /// that go to the storage service. The one supplied must not follow redirects; see
286    /// [`HttpClient`]. The timeout set on the builder is then ignored — a timeout belongs to
287    /// the client that can enforce it.
288    #[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    /// What the client calls itself in `User-Agent`.
295    #[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    /// How long the HTTP client the SDK ships gives an answer to arrive. It has no effect on
302    /// one supplied with [`ClientBuilder::http_client`]. This bounds one request on the
303    /// wire; the whole of an operation is bounded by [`ClientBuilder::operation_timeout`].
304    #[must_use]
305    pub fn timeout(mut self, timeout: Duration) -> ClientBuilder {
306        self.timeout = timeout;
307        self
308    }
309
310    /// The most an operation may take from the call to its answer, everything the client
311    /// waits for included: waiting at the gate, fetching credentials, every attempt, every
312    /// wait between them, the resend after a refresh, and reading the body. Past it the
313    /// operation ends as a retryable network error, and whatever it was doing is dropped —
314    /// a permit it held goes back, and the hooks hear it end. Decoding the answer into the
315    /// caller's type comes after, on the caller's own thread, and is not waited for. None
316    /// by default: an operation may then take as long as its attempts and waits add up to,
317    /// each attempt bounded only by the HTTP client's own [`ClientBuilder::timeout`].
318    #[must_use]
319    pub fn operation_timeout(mut self, limit: Duration) -> ClientBuilder {
320        self.operation_timeout = Some(limit);
321        self
322    }
323
324    /// The most times any operation is resent after a transient failure. A modelled
325    /// route is resent as many times as its own policy allows and no more; this only
326    /// lowers that. A path the caller wrote, which no policy covers, is resent this many
327    /// times when its method is idempotent.
328    #[must_use]
329    pub fn max_retries(mut self, max_retries: u32) -> ClientBuilder {
330        self.max_retries = max_retries;
331        self
332    }
333
334    /// The least the client waits before the first resend. A modelled route starts from
335    /// the delay its own policy names when that is longer; a path the caller wrote starts
336    /// from this, or from [`DEFAULT_BASE_DELAY`] when it is not set. Each wait after the
337    /// first is double the one before. [`ClientBuilder::max_delay`] holds every wait down,
338    /// this one included.
339    #[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    /// The most the client waits between attempts, jitter included, whatever the policy,
346    /// the backoff or [`ClientBuilder::base_delay`] asks for. The wait a `Retry-After`
347    /// names is honoured as given.
348    #[must_use]
349    pub fn max_delay(mut self, max_delay: Duration) -> ClientBuilder {
350        self.max_delay = max_delay;
351        self
352    }
353
354    /// The most added at random to each wait, so resends from many clients do not land
355    /// together.
356    #[must_use]
357    pub fn max_jitter(mut self, max_jitter: Duration) -> ClientBuilder {
358        self.max_jitter = max_jitter;
359        self
360    }
361
362    /// How many pages [`Client::each_page`] reads before it stops. Zero is refused by
363    /// [`ClientBuilder::build`].
364    #[must_use]
365    pub fn max_pages(mut self, max_pages: usize) -> ClientBuilder {
366        self.max_pages = max_pages;
367        self
368    }
369
370    /// The most a JSON or HTML answer may deliver before the client refuses to hold it.
371    /// Zero asks for the default: the cap cannot be lifted, only moved.
372    #[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    /// Caches JSON reads by `ETag`. Without this, `config.cache_enabled` decides whether a
379    /// [`FileCache`] in `config.cache_dir` is used.
380    #[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    /// Reports every operation and every request the client makes. Several sets of hooks
387    /// go on as one with [`crate::observability::ChainHooks`].
388    #[must_use]
389    pub fn hooks(mut self, hooks: impl Hooks + 'static) -> ClientBuilder {
390        self.hooks = Arc::new(hooks);
391        self
392    }
393
394    /// The client, or a usage error for a builder without credentials, with no timeout, or
395    /// with no pages to read.
396    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    /// A [`ClientBuilder`] for `config`, at the defaults.
464    pub fn builder(config: Config) -> ClientBuilder {
465        ClientBuilder::new(config)
466    }
467
468    /// A client with the default settings and a bearer token, on the HTTP client the SDK
469    /// ships. Without the `reqwest` feature there is no such client, and a
470    /// [`ClientBuilder`] with an [`HttpClient`] of the application's own is the way in.
471    #[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    /// The configuration this client was built from.
478    pub fn config(&self) -> &Config {
479        &self.shared.config
480    }
481
482    /// Where HEY is, with the trailing slash every path is joined to.
483    pub fn base_url(&self) -> &Url {
484        &self.shared.base_url
485    }
486
487    /// The linked account this client presents, or `None` for All Accounts.
488    pub fn account_id(&self) -> Option<i64> {
489        self.account_id
490    }
491
492    /// How many pages a walk reads before it stops.
493    pub fn max_pages(&self) -> usize {
494        self.shared.max_pages
495    }
496
497    /// The HTTP client every request goes out on, for the one request the SDK makes outside
498    /// HEY: the attachment blob that goes to the storage service the direct upload named. It
499    /// shares the connection pool and the settings the caller configured, and carries no
500    /// credentials of its own — those go on per request.
501    pub(crate) fn http(&self) -> &dyn HttpClient {
502        self.shared.http.as_ref()
503    }
504
505    /// Starts a request for one of the modelled routes. Generated service methods call
506    /// this; reach for it directly only to add headers or query parameters they do not
507    /// expose.
508    pub fn operation(&self, route: &'static Route, params: &[&dyn Display]) -> Operation {
509        Operation::for_route(route, params)
510    }
511
512    /// Starts a request for a path the model does not cover. The path is relative to the
513    /// base URL and gets the same credentials, `.json` suffix, account scope and retry
514    /// treatment as a modelled one.
515    pub fn request(&self, method: Method, path: impl Into<String>) -> Operation {
516        Operation::raw(method, path.into())
517    }
518
519    /// Sends an operation and decodes its JSON body.
520    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    /// Sends an operation whose answer carries no body worth reading.
529    pub async fn send_unit(&self, operation: Operation) -> Result<(), Error> {
530        self.execute(operation).await.map(|_| ())
531    }
532
533    /// Sends an operation and reads its body as text: the HTML page a route serves no
534    /// JSON for.
535    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    /// Sends an operation that answers a status meaning "nothing there" with `None`.
541    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    /// Sends a paginated read and keeps the cursor HEY answered with.
558    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    /// Reads the page after the given one, or `None` when HEY named no next page. A
571    /// `Link` header pointing off the HEY origin is refused rather than followed. The read
572    /// announces itself as the operation the first page came from, so a whole walk shows
573    /// up as one thing rather than as a list read followed by anonymous requests, and it
574    /// is resent under that operation's retry policy.
575    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    /// Reads every page after the first, calling `visit` with each one. Stops early when
594    /// `visit` answers `false`. A walk that reaches the client's page limit with pages
595    /// still to read stops there and says so, as [`Error::pagination_capped`]: the pages
596    /// visited stand, and the caller knows they were not all of them.
597    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    /// Sends an operation: asks the hooks whether it may run, applies credentials and
624    /// account scope, retries transient failures when the operation is idempotent,
625    /// resends once after a refreshed 401, and answers a cached body on 304. Non-2xx
626    /// statuses become errors unless the operation treats them as empty.
627    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    /// Sends an operation and hands back the answer with its body unread, for a caller
635    /// that writes it somewhere rather than holding it. Everything up to the answer is
636    /// [`Client::execute`]'s doing — the gate, the credentials, the account scope, the
637    /// retries, the resend after a refreshed 401 — and nothing is resent once the answer
638    /// is in hand, since its bytes may already be on their way out. The deadline is the
639    /// caller's to hold, so the bytes it goes on to read can be held to the same one.
640    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    /// When the operation in progress has to be over: the deadline of the operation this
651    /// task is already inside, when it is inside one, or else
652    /// [`ClientBuilder::operation_timeout`] from now; `None` when there is no limit.
653    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    /// Holds some work to [`ClientBuilder::operation_timeout`] as one operation: every
664    /// request made inside it — a convenience's follow-up, a walk's later pages — shares
665    /// the one deadline rather than starting a limit of its own.
666    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    /// Holds some work to a deadline. Past it the work is dropped where it stands — which
677    /// is what makes the guards report the ends they owe, and the resilience layer give
678    /// back what the operation held — and the caller gets a network error naming the
679    /// limit.
680    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    /// Runs some work as one operation: the gate, the span, the start, and the end with
697    /// the work's outcome. For a hand-written convenience whose answer is not what HEY
698    /// answered — a changes feed's 409 that comes back as a full-sync answer, a refusal
699    /// reworded from the body it arrived in — so that the hooks hear the operation end the
700    /// way the caller sees it. The sends inside are marked [`Operation::quiet`], which
701    /// leaves the request hooks firing for each one and announces no operation of their
702    /// own; a quiet send made in here records its answer on this operation's span.
703    ///
704    /// The work is held to the one deadline [`Client::execute`] would hold it to, and
705    /// every send inside inherits that deadline rather than starting one of its own, so an
706    /// operation that runs out of time ends with the same [`Error::timed_out`] whichever
707    /// side notices first.
708    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    /// Runs one operation inside the hook lifecycle every call shares. A quiet operation is
723    /// one request inside another and skips that lifecycle — see [`Operation::quiet`].
724    /// The `tracing` span around all of this is the caller's to put on, so that the gate and
725    /// the end hook are inside it too.
726    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    /// The hook lifecycle itself: the gate, the start, the work, and the end with how the
740    /// work went.
741    ///
742    /// The deadline is applied in here, to the gate and to the work, so that the hooks hear
743    /// an operation that ran out of time end with the same [`Error::timed_out`] the caller
744    /// gets. The end is reported from a drop guard rather than after the await all the
745    /// same, because the await may never return: a caller's own `tokio::time::timeout` or
746    /// `select!` can drop the future mid-flight, and a start with no end leaves the
747    /// bulkhead a permit short and the circuit breaker a call short for the life of the
748    /// client. Dropped that way, the operation ends as [`Error::cancelled`].
749    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    /// Reads the answer the retry loop settled on, and tells the hooks how it turned out
771    /// once its body has been dealt with.
772    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    /// Hands the answer over unread, once its status says there is a body worth reading.
802    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    /// What the retry loop may spend on one operation. A modelled route brings its own
829    /// policy from the model — how many sends it gets in all, which statuses earn another,
830    /// and how long the first wait is — and the client's settings only make that gentler:
831    /// [`ClientBuilder::max_retries`] caps the sends, [`ClientBuilder::base_delay`] holds
832    /// the wait up and [`ClientBuilder::max_delay`] holds it down. A route the model gives
833    /// no policy is sent once. A path the caller wrote has no policy to bring, so it runs
834    /// on the client's settings alone. Whatever the policy, an operation that is not
835    /// idempotent is sent once.
836    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    /// Sends the operation as many times as its retry budget and HEY's answers call for,
861    /// and hands back the answer it stopped on with the body still unread.
862    #[allow(clippy::too_many_lines)] // one loop, read as one: every way out of an attempt is in view
863    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        // Looked up once and carried across the attempts: a resend would find the same
871        // entry, and the cache the SDK ships reads it off disk.
872        let mut cached = None;
873
874        loop {
875            // Signed and counted under the read half of the refresh lock, so no refresh
876            // lands between the two: the counts say exactly which credentials went out.
877            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            // The attempt span closes here, before any refresh or backoff: it is the send.
891            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                    // A 401 from a hop that carried no credentials rejected none of HEY's:
928                    // there is nothing to refresh, and nothing a resend would change.
929                    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                        // A `Retry-After` on any status that earns a resend: a 503 says how
971                        // long the outage is expected to last as plainly as a 429 says how
972                        // long to back off. One that asks for nothing, or that could not be
973                        // read, leaves the backoff to decide.
974                        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                        // An answer reached through a redirect is another resource's: the
984                        // entry looked up for the URL asked for neither satisfies its 304
985                        // nor takes its body.
986                        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    /// Answers a 401 with fresh credentials, once for all the requests the stale ones
1007    /// earned it on. Refreshes go one at a time, and a request that was signed before the
1008    /// last refresh is simply resent: the credentials it will pick up are already the new
1009    /// ones, and asking the token endpoint again would only spend a round trip — or, with
1010    /// a rotating refresh token, burn the one just issued. A refresh that fails is shared
1011    /// the same way: every request signed with the credentials it could not renew takes
1012    /// the failure as its answer rather than refreshing again, since its credentials are
1013    /// the very ones the token endpoint just declined to renew, so an outage there costs
1014    /// one call per set of credentials rather than one per request. Only a request signed
1015    /// after the failure asks again: its 401 is news.
1016    ///
1017    /// A renewal the provider made of its own accord is a refresh too. Before the SDK's own
1018    /// bearer strategy is asked to refresh, it is asked what it would sign with now, and a
1019    /// token other than the `rejected` one — the bearer the 401 came back on — is a renewal
1020    /// already made: the counts move as for a refresh, the request is resent with it, and
1021    /// the provider is not asked, since a refresh token it was just issued would be spent
1022    /// again over the top of the token it issued. Only a token the provider would still sign
1023    /// with is refreshed. A provider that cannot hand over a token at all when asked is
1024    /// taken as the refresh failing, shared like any other failure, and its refresh is not
1025    /// asked for on top. A strategy of the caller's leaves no `rejected` bearer and is asked
1026    /// outright.
1027    ///
1028    /// The refresh runs on a task of its own, which holds the turn, so a caller that gives
1029    /// up waiting — its [`ClientBuilder::operation_timeout`] running out, say — does not
1030    /// abandon a refresh the token endpoint may already have honoured: the provider still
1031    /// gets to keep what it was handed, the count still moves, and the next caller waits
1032    /// its turn rather than refreshing again over the top of it.
1033    /// A caller gone before its refresh got the turn does not have one started on its
1034    /// behalf: a queue of stale requests whose limits ran out while an earlier refresh
1035    /// held the turn would otherwise each refresh in turn, for nobody.
1036    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            // Every 401 gets a task and a turn of its own, and each reads the counts
1046            // against the generation its own request was signed under only once it holds
1047            // the turn — there is no refresh in flight to join, and no answer to take but
1048            // the one worked out here, now. So a request that came out of its signing
1049            // under a newer generation than a refresh running for an older one cannot be
1050            // handed that refresh's answer: its turn comes after, and finds the counts the
1051            // older refresh left, which say whether its own credentials were renewed.
1052            let _turn = shared.refreshing.write().await;
1053            if shared.refreshes.load(Ordering::Acquire) != signed_under.refreshes {
1054                // Renewed since the signing, by someone else's refresh or by the provider
1055                // on its own: resend on them.
1056                true
1057            } else if shared.refresh_runs.load(Ordering::Acquire) != signed_under.runs
1058                || !wanted.load(Ordering::Acquire)
1059            {
1060                // A refresh of these very credentials already ran and failed, so its
1061                // answer is this request's too — or the caller is gone, and a refresh
1062                // started now would be for nobody.
1063                false
1064            } else {
1065                let renewed = match rejected {
1066                    Some(rejected) => match shared.bearer_now().await {
1067                        // The provider has replaced the rejected token already: that is
1068                        // the renewal, and asking for another would spend it.
1069                        Ok(Some(now)) if now != rejected => true,
1070                        Ok(_) => shared.auth.refresh().await,
1071                        // The provider could not hand over a token at all — its own
1072                        // renewal failing, as often as not — so that is this refresh's
1073                        // answer, shared like any other, rather than a second attempt.
1074                        Err(_) => false,
1075                    },
1076                    None => shared.auth.refresh().await,
1077                };
1078                if renewed {
1079                    shared.refreshes.fetch_add(1, Ordering::AcqRel);
1080                    // The next signing carries the renewed token; that is this refresh,
1081                    // already counted, not another. No signer holds the mutex, since the
1082                    // write half is held here.
1083                    *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    /// Builds the request for one attempt, signs it, and looks the response cache up the
1124    /// first time it is asked for a key. `cached` carries the entry — or the empty stand-in
1125    /// that says "cacheable, nothing stored" — from one attempt to the next. Called under
1126    /// the read half of [`Shared::refreshing`], so the counts the signing reads are those
1127    /// of the credentials it put on.
1128    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        // A refreshed credential gives the read a new key, and whatever was held under the
1166        // old one belongs to somebody else's reading.
1167        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    /// Puts the credentials on a request and says what it went out under: the strategy is
1180    /// asked, its answer compared with the last, and the counts read, all under
1181    /// [`Shared::signing`], so no other signing comes between the taking of the token and
1182    /// the counts it is recorded against. A bearer the SDK's own strategy put on that is
1183    /// not the one it last put on is a renewal the provider made on its own, and moves the
1184    /// counts before they are read: the request goes out under the renewed credentials, and
1185    /// every request signed with the old bearer is resent rather than refreshed.
1186    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                // A renewal that ran to an answer, as much as a refresh that did.
1197                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    /// What the cache holds for a key, as the attempt should carry it: the stored entry, an
1209    /// empty stand-in when there is nothing stored, and nothing at all when what is stored
1210    /// is longer than the client would hold — which is thrown away on the way past.
1211    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    /// The cache the operation reads and writes, when there is one to use. Cached bodies
1235    /// are held per identity, so a request that goes out without credentials — an
1236    /// [`AuthStrategy`] that signs some other way, or none at all — is not cached: there
1237    /// would be nothing to tell one caller's copy from another's.
1238    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    /// Sends one request and follows the redirects it is answered with, up to
1250    /// [`MAX_REDIRECTS`] hops, unless the operation is one that takes the redirect for its
1251    /// answer. Hands back the answer with the URL it came from, whether a redirect was
1252    /// followed on the way, and whether the hop it came from carried the credentials.
1253    ///
1254    /// Credentials stay on the origin they were meant for: a hop to another origin goes out
1255    /// without the `Authorization`, the way a browser would send it, which is how a blob
1256    /// request ends up at the storage service without HEY's token. A 303 turns anything but
1257    /// a GET or HEAD into a GET without its body, a 301 or 302 does that to a POST alone,
1258    /// and a 307 or 308 keeps both.
1259    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                    // The credentials come off on the way to another origin and do not go
1301                    // back on for a hop that returns: from here on nothing is signed.
1302                    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    /// The most of an answer to this operation the client will hold. An answer it asked
1314    /// for as a document it goes on to parse is held to the configured cap; anything else
1315    /// — a blob, an export, whatever a form request answered — to the fixed
1316    /// [`MAX_RESPONSE_BODY_BYTES`]. What the server labels the answer does not come into
1317    /// it: a JSON body sent as a PNG is still capped, and an attachment that turns out to
1318    /// be text is still not.
1319    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            // The status is what matters about a failure, and a body the client would not
1361            // read is no reason to lose it.
1362            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    /// Sleeps the backoff's wait plus a little jitter, held under the longest wait the
1412    /// client allows.
1413    async fn wait(&self, delay: Duration) {
1414        tokio::time::sleep((delay + self.jitter()).min(self.shared.max_delay)).await;
1415    }
1416
1417    /// Sleeps the wait HEY asked for, which the client's ceiling does not shorten: the
1418    /// server said when it will answer again, and resending sooner only earns another
1419    /// refusal.
1420    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
1438/// An operation the hooks have been told the start of and are still owed the end of. It
1439/// reports the end whichever way the operation leaves: [`Running::finished`] with the
1440/// outcome, or the drop that comes instead when the caller abandons the future.
1441struct 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        // A state still here is one `finished` never took, which means the future was
1460        // dropped before the work returned.
1461        if self.state.is_some() {
1462            self.finished(Err(&Error::cancelled()));
1463        }
1464    }
1465}
1466
1467/// What one operation may spend on being resent: the sends it gets in all, the statuses
1468/// that earn another, and the wait before the first resend.
1469struct Budget {
1470    attempts: u32,
1471    retry_on: &'static [u16],
1472    delay: Duration,
1473}
1474
1475/// The credentials a request went out with, as the counts at its signing: how many
1476/// refreshes had renewed them, and how many had run at all. The first says whether a 401
1477/// is already answered by someone else's refresh; the second whether a refresh of these
1478/// very credentials already ran and failed, in which case its answer is this request's too.
1479#[derive(Clone, Copy)]
1480struct Generation {
1481    refreshes: u64,
1482    runs: u64,
1483}
1484
1485/// How a request went out: the counts at its signing, and the bearer the SDK's own
1486/// strategy put on it — `None` under a strategy of the caller's, whose headers are not
1487/// read — for a 401 to be checked against what the provider would sign with now.
1488struct Signed {
1489    under: Generation,
1490    bearer: Option<HeaderValue>,
1491}
1492
1493/// Whether the caller that asked for a refresh is still there to want it. Dropped when
1494/// that caller's future is — its limit running out, a `select!` taking another branch —
1495/// so a refresh that has not yet had its turn can stand down.
1496struct 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
1514/// A request the hooks have been told the start of and are still owed the end of, the
1515/// way [`Running`] is for an operation. It reports the end from [`Sending::end`] with how
1516/// the request turned out, or from the drop that comes instead when the future is
1517/// abandoned mid-request — an operation limit running out, a caller's `select!` — so a
1518/// hook counting requests in flight is never left one short.
1519struct 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
1558/// What one send came back with: the answer, the URL it came from once any redirects were
1559/// followed, whether any were, and whether the hop that answered went out with the
1560/// credentials — which it did not once any hop left the origin, since they do not come
1561/// back for one that returns.
1562struct Received {
1563    url: Url,
1564    response: HttpResponse<Body>,
1565    redirected: bool,
1566    authenticated: bool,
1567}
1568
1569/// One answer from HEY with its body unread: what the retry loop settled on, the URL it
1570/// came from once any redirects were followed, and what the hooks still have to be told
1571/// about it once the body has been dealt with.
1572struct 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
1582/// The cache is whatever the caller supplied, and the one the SDK ships keeps its entries in
1583/// files. So every read and write of it goes to the blocking pool: a file read on the
1584/// runtime's own thread stalls every other task sharing that thread. A cache that cannot be
1585/// reached — the pool shutting down under it — is a miss, which is what any other failure to
1586/// read it is too.
1587async 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
1620/// Where a redirect points, when the answer is one and says where. A 3xx without a
1621/// `Location`, or with one that is not a URL, is handed back as the answer it is.
1622fn 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
1635/// The request to send to `next` on the way there from `from`: the same one, less the
1636/// cache validator, less the credentials when the origin changes, and reduced to a GET
1637/// when the status asks for it.
1638fn 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    // The validator was the resource asked for's; the one pointed to has its own.
1652    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
1667/// Whether a redirect is followed with the request as it was, or as a GET without its body.
1668/// A 303 says fetch the answer, whatever the method; a 301 or 302 is only allowed to turn a
1669/// POST into a GET, and leaves a PUT, PATCH or DELETE as it was, since a GET in its place
1670/// would report a mutation done that never reached where it was sent; a 307 or 308 keeps
1671/// everything.
1672fn 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
1690/// HEY answers JSON to paths that end in `.json`. The model leaves the extension off
1691/// paths that end in a parameter, since Smithy cannot express `{id}.json`, so it is put
1692/// back here unless the last segment already carries an extension.
1693pub(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
1702/// The span an operation runs in: one of its own, or for a quiet send — one request inside
1703/// another operation — that operation's own span when it is running inside
1704/// [`Client::as_operation`], and otherwise none, so it runs in whatever span its caller is in.
1705fn 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
1715/// HEY's own id for the request, when the answer names one.
1716fn request_id(headers: &HeaderMap) -> Option<&str> {
1717    headers
1718        .get("x-request-id")
1719        .and_then(|value| value.to_str().ok())
1720}
1721
1722/// The wait HEY asked for, on a status that earns a resend. A `Retry-After` on any other
1723/// answer is not a wait the client will take, so it is not one it reports.
1724fn 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
1737/// Whether the answer to a request that asked for this is a document the SDK buffers and
1738/// parses. Anything it did not ask for as JSON or HTML — a blob's `*/*`, an export's
1739/// `text/csv` — it streams or holds under its own bound instead.
1740fn 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
1750/// Reads a body up to the bound and refuses it on the first byte past. A body exactly at
1751/// the bound reads whole; one declared past it never starts.
1752pub(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    /// An [`HttpClient`] with no network behind it: it answers each request from a closure
1775    /// and keeps what it was sent. This is the second implementation the trait exists for,
1776    /// so the client is exercised here with no `reqwest` in the picture.
1777    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    /// A provider whose token can be renewed, and which counts how often it was asked to.
1870    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    /// Sends a write with a JSON body to `/old`, answered by a redirect of the given status
1944    /// to `/new`, and hands back the request the second hop went out as.
1945    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    /// A 301 or 302 may only turn a POST into a GET: a PUT, PATCH or DELETE followed as a
1969    /// GET would report a mutation done that never reached where it was sent.
1970    #[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    /// A 303 says fetch the answer, whatever was sent.
1997    #[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        // /a is answered, then redirected to /b, then answered 304. /b carries the same
2103        // ETag, so an entry that took b's body under a's key would pass the 304 off as a.
2104        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}