Skip to main content

cratefield_oauth_client/
refresh.rs

1//! The one 401 policy worth sharing: a rejected call gets one refresh and a
2//! replay, and only then gives up. Never a loop — a provider that answers
3//! 401 twice in a row is not answered by a third try.
4
5use bytes::Bytes;
6use cratefield_core::{HttpClient, HttpError};
7use http::Request;
8use thiserror::Error;
9
10/// What a [`send_with_refresh`] call can end as. The replay's own outcome —
11/// whatever status the provider answered it with — is a success here: a 401
12/// on the replay is data for the caller's token layer, not this helper's
13/// error to name.
14#[derive(Debug, Error)]
15pub enum SendWithRefreshError<E> {
16    /// The transport refused a send (either one).
17    #[error("transport: {0}")]
18    Transport(#[from] HttpError),
19    /// The refresh did not produce a token, so there was no replay. The
20    /// caller's own refresh error is carried untouched.
21    #[error("the access token was rejected and the refresh failed")]
22    Refresh(E),
23}
24
25/// Sends a bearer-authenticated request; if the provider answers 401,
26/// refreshes the token once and replays the request with it.
27///
28/// - `build` makes the request from an access token. It is called at most
29///   twice: once with the token it is handed, once with the refreshed one.
30/// - `refresh` is called at most once, and only after a 401. Its error is
31///   returned as [`SendWithRefreshError::Refresh`] and the request is never
32///   retried — what a failed refresh means (reconnect, back off) is the
33///   caller's decision, not this helper's.
34///
35/// # Errors
36///
37/// A refused send, either one, as [`SendWithRefreshError::Transport`]; a
38/// refresh that did not produce a token as
39/// [`SendWithRefreshError::Refresh`]. The replay's status is returned as
40/// data even when it is another 401.
41pub async fn send_with_refresh<E, R>(
42    http: &dyn HttpClient,
43    access_token: &str,
44    build: impl Fn(&str) -> Request<Bytes>,
45    refresh: impl FnOnce() -> R,
46) -> Result<http::Response<Bytes>, SendWithRefreshError<E>>
47where
48    R: Future<Output = Result<String, E>>,
49{
50    let response = http.send(build(access_token)).await?;
51    if response.status() != http::StatusCode::UNAUTHORIZED {
52        return Ok(response);
53    }
54    let fresh = refresh().await.map_err(SendWithRefreshError::Refresh)?;
55    Ok(http.send(build(&fresh)).await?)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use async_trait::async_trait;
62    use std::sync::Arc;
63    use std::sync::atomic::{AtomicUsize, Ordering};
64
65    /// A refresh that counts it ran and always produces a token.
66    fn refreshing(
67        count: Arc<AtomicUsize>,
68    ) -> impl FnOnce() -> std::future::Ready<Result<String, ()>> {
69        move || {
70            count.fetch_add(1, Ordering::Relaxed);
71            std::future::ready(Ok("fresh-token".to_owned()))
72        }
73    }
74
75    /// A refresh that never produces a token.
76    async fn refusing() -> Result<String, ()> {
77        Err(())
78    }
79
80    /// Answers statuses from a script, in order, and counts the sends.
81    struct Scripted {
82        statuses: Vec<u16>,
83        next: AtomicUsize,
84        sends: AtomicUsize,
85    }
86
87    impl Scripted {
88        fn with(statuses: &[u16]) -> Self {
89            Self {
90                statuses: statuses.to_vec(),
91                next: AtomicUsize::new(0),
92                sends: AtomicUsize::new(0),
93            }
94        }
95
96        fn sends(&self) -> usize {
97            self.sends.load(Ordering::Relaxed)
98        }
99    }
100
101    #[async_trait]
102    impl HttpClient for Scripted {
103        async fn send(&self, _request: Request<Bytes>) -> Result<http::Response<Bytes>, HttpError> {
104            self.sends.fetch_add(1, Ordering::Relaxed);
105            let index = self.next.fetch_add(1, Ordering::Relaxed);
106            let status = self.statuses.get(index).copied().unwrap_or(500);
107            Ok(http::Response::builder()
108                .status(status)
109                .body(Bytes::new())
110                .expect("status"))
111        }
112    }
113
114    fn request_for(token: &str) -> Request<Bytes> {
115        Request::builder()
116            .uri("https://provider.example/things")
117            .header("authorization", format!("Bearer {token}"))
118            .body(Bytes::new())
119            .expect("builds")
120    }
121
122    #[pollster::test]
123    async fn a_401_gets_one_refresh_and_one_replay() {
124        let http = Scripted::with(&[401, 201]);
125        let refreshed = Arc::new(AtomicUsize::new(0));
126        let response = send_with_refresh(
127            &http,
128            "stale-token",
129            request_for,
130            refreshing(Arc::clone(&refreshed)),
131        )
132        .await
133        .expect("the replay succeeds");
134        assert_eq!(response.status(), 201);
135        assert_eq!(refreshed.load(Ordering::Relaxed), 1, "exactly one refresh");
136        assert_eq!(http.sends(), 2, "one send, one replay");
137    }
138
139    #[pollster::test]
140    async fn a_failed_refresh_ends_the_sequence_without_a_replay() {
141        let http = Scripted::with(&[401]);
142        let error = send_with_refresh(&http, "stale-token", request_for, refusing)
143            .await
144            .expect_err("the refresh failed");
145        assert!(
146            matches!(error, SendWithRefreshError::Refresh(())),
147            "{error:?}"
148        );
149        assert_eq!(http.sends(), 1, "no replay after a failed refresh");
150    }
151
152    #[pollster::test]
153    async fn a_401_on_the_replay_is_data_not_a_loop() {
154        let http = Scripted::with(&[401, 401]);
155        let refreshed = Arc::new(AtomicUsize::new(0));
156        let response = send_with_refresh(
157            &http,
158            "stale-token",
159            request_for,
160            refreshing(Arc::clone(&refreshed)),
161        )
162        .await
163        .expect("the replay's status is returned, however unhelpful");
164        assert_eq!(response.status(), 401);
165        assert_eq!(
166            refreshed.load(Ordering::Relaxed),
167            1,
168            "never a second refresh"
169        );
170        assert_eq!(http.sends(), 2, "never a third send");
171    }
172
173    #[pollster::test]
174    async fn a_non_401_never_touches_the_refresh() {
175        let http = Scripted::with(&[403]);
176        let response = send_with_refresh(&http, "token", request_for, refusing)
177            .await
178            .expect("the status is passed through");
179        assert_eq!(response.status(), 403);
180        assert_eq!(http.sends(), 1);
181    }
182}