dropbox_sdk/
default_client.rs

1// Copyright (c) 2020-2025 Dropbox, Inc.
2
3//! The default HTTP client.
4//!
5//! Use this client if you're not particularly picky about implementation details, as the specific
6//! implementation is not exposed, and may be changed in the future.
7//!
8//! If you have a need for a specific HTTP client implementation, or your program is already using
9//! some HTTP client crate, you probably want to have this Dropbox SDK crate use it as well. To do
10//! that, you should implement the traits in `crate::client_trait` for it and use it instead.
11//!
12//! This code (and its dependencies) are only built if you use the `default_client` Cargo feature.
13
14use crate::client_trait::{
15    AppAuthClient, HttpClient, HttpRequest, HttpRequestResultRaw, NoauthClient, TeamAuthClient,
16    TeamSelect, UserAuthClient,
17};
18use crate::default_client_common::impl_set_path_root;
19use crate::oauth2::{Authorization, TokenCache};
20use crate::Error;
21use futures::FutureExt;
22use std::str::FromStr;
23use std::sync::Arc;
24use ureq::typestate::WithBody;
25use ureq::Agent;
26
27macro_rules! impl_update_token {
28    ($self:ident) => {
29        fn update_token(&$self, old_token: Arc<String>) -> Result<bool, Error> {
30            info!("refreshing auth token");
31            match $self.tokens.update_token(
32                TokenUpdateClient { inner: &$self.inner },
33                old_token,
34            ).now_or_never().unwrap() {
35                Ok(_) => Ok(true),
36                Err(e) => {
37                    error!("failed to update auth token: {e}");
38                    Err(e.into())
39                }
40            }
41        }
42    };
43}
44
45/// Default HTTP client using User authorization.
46pub struct UserAuthDefaultClient {
47    inner: UreqClient,
48    tokens: Arc<TokenCache>,
49    path_root: Option<String>, // a serialized PathRoot enum
50}
51
52impl UserAuthDefaultClient {
53    /// Create a new client using the given OAuth2 authorization.
54    pub fn new(auth: Authorization) -> Self {
55        Self::from_token_cache(Arc::new(TokenCache::new(auth)))
56    }
57
58    /// Create a new client from a [`TokenCache`], which lets you share the same tokens between
59    /// multiple clients.
60    pub fn from_token_cache(tokens: Arc<TokenCache>) -> Self {
61        Self {
62            inner: UreqClient::default(),
63            tokens,
64            path_root: None,
65        }
66    }
67
68    impl_set_path_root!(self);
69}
70
71impl HttpClient for UserAuthDefaultClient {
72    type Request = UreqRequest;
73
74    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
75        self.inner.execute(request, body)
76    }
77
78    fn new_request(&self, url: &str) -> Self::Request {
79        self.inner.new_request(url)
80    }
81
82    impl_update_token!(self);
83
84    fn token(&self) -> Option<Arc<String>> {
85        self.tokens.get_token()
86    }
87
88    fn path_root(&self) -> Option<&str> {
89        self.path_root.as_deref()
90    }
91}
92
93impl UserAuthClient for UserAuthDefaultClient {}
94
95/// Default HTTP client using Team authorization.
96pub struct TeamAuthDefaultClient {
97    inner: UreqClient,
98    tokens: Arc<TokenCache>,
99    path_root: Option<String>, // a serialized PathRoot enum
100    team_select: Option<TeamSelect>,
101}
102
103impl TeamAuthDefaultClient {
104    /// Create a new client using the given OAuth2 token, with no user/admin context selected.
105    pub fn new(tokens: impl Into<Arc<TokenCache>>) -> Self {
106        Self {
107            inner: UreqClient::default(),
108            tokens: tokens.into(),
109            path_root: None,
110            team_select: None,
111        }
112    }
113
114    /// Select a user or team context to operate in.
115    pub fn select(&mut self, team_select: Option<TeamSelect>) {
116        self.team_select = team_select;
117    }
118
119    impl_set_path_root!(self);
120}
121
122impl HttpClient for TeamAuthDefaultClient {
123    type Request = UreqRequest;
124
125    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
126        self.inner.execute(request, body)
127    }
128
129    fn new_request(&self, url: &str) -> Self::Request {
130        self.inner.new_request(url)
131    }
132
133    fn token(&self) -> Option<Arc<String>> {
134        self.tokens.get_token()
135    }
136
137    impl_update_token!(self);
138
139    fn path_root(&self) -> Option<&str> {
140        self.path_root.as_deref()
141    }
142
143    fn team_select(&self) -> Option<&TeamSelect> {
144        self.team_select.as_ref()
145    }
146}
147
148impl TeamAuthClient for TeamAuthDefaultClient {}
149
150/// Default HTTP client using App authorization.
151#[derive(Debug)]
152pub struct AppAuthDefaultClient {
153    inner: UreqClient,
154    path_root: Option<String>,
155    auth: String,
156}
157
158impl AppAuthDefaultClient {
159    /// Create a new App auth client using the given app key and secret, which can be found in the Dropbox app console.
160    pub fn new(app_key: &str, app_secret: &str) -> Self {
161        use base64::prelude::*;
162        let encoded = BASE64_STANDARD.encode(format!("{app_key}:{app_secret}"));
163        Self {
164            inner: UreqClient::default(),
165            path_root: None,
166            auth: format!("Basic {encoded}"),
167        }
168    }
169
170    impl_set_path_root!(self);
171}
172
173impl HttpClient for AppAuthDefaultClient {
174    type Request = UreqRequest;
175
176    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
177        self.inner.execute(request, body)
178    }
179
180    fn new_request(&self, url: &str) -> Self::Request {
181        self.inner
182            .new_request(url)
183            .set_header("Authorization", &self.auth)
184    }
185}
186
187impl AppAuthClient for AppAuthDefaultClient {}
188
189/// Default HTTP client for unauthenticated API calls.
190#[derive(Debug, Default)]
191pub struct NoauthDefaultClient {
192    inner: UreqClient,
193    path_root: Option<String>,
194}
195
196impl NoauthDefaultClient {
197    impl_set_path_root!(self);
198}
199
200impl HttpClient for NoauthDefaultClient {
201    type Request = UreqRequest;
202
203    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
204        self.inner.execute(request, body)
205    }
206
207    fn new_request(&self, url: &str) -> Self::Request {
208        self.inner.new_request(url)
209    }
210
211    fn path_root(&self) -> Option<&str> {
212        self.path_root.as_deref()
213    }
214}
215
216impl NoauthClient for NoauthDefaultClient {}
217
218/// Same as NoauthDefaultClient but with inner by reference and no path_root.
219/// Only used for updating authorization tokens.
220struct TokenUpdateClient<'a> {
221    inner: &'a UreqClient,
222}
223
224impl HttpClient for TokenUpdateClient<'_> {
225    type Request = UreqRequest;
226
227    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
228        self.inner.execute(request, body)
229    }
230
231    fn new_request(&self, url: &str) -> Self::Request {
232        self.inner.new_request(url)
233    }
234}
235
236impl crate::async_client_trait::NoauthClient for TokenUpdateClient<'_> {}
237
238#[derive(Debug)]
239struct UreqClient {
240    agent: Agent,
241}
242
243impl Default for UreqClient {
244    fn default() -> Self {
245        Self {
246            agent: Agent::new_with_config(
247                Agent::config_builder()
248                    .https_only(true)
249                    .http_status_as_error(false)
250                    .build(),
251            ),
252        }
253    }
254}
255
256impl HttpClient for UreqClient {
257    type Request = UreqRequest;
258
259    fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
260        let resp = if body.is_empty() {
261            request.req.send_empty()
262        } else {
263            request.req.send(body)
264        };
265
266        let (status, resp) = match resp {
267            Ok(resp) => (resp.status().as_u16(), resp),
268            Err(ureq::Error::Io(e)) => {
269                return Err(e.into());
270            }
271            Err(e) => {
272                return Err(RequestError { inner: e }.into());
273            }
274        };
275
276        let result_header = resp
277            .headers()
278            .get("Dropbox-API-Result")
279            .map(|v| String::from_utf8(v.as_bytes().to_vec()))
280            .transpose()
281            .map_err(|e| e.utf8_error())?;
282
283        let content_length = resp
284            .headers()
285            .get("Content-Length")
286            .map(|v| {
287                let s = std::str::from_utf8(v.as_bytes())?;
288                u64::from_str(s).map_err(|e| {
289                    Error::UnexpectedResponse(format!("invalid Content-Length {s:?}: {e}"))
290                })
291            })
292            .transpose()?;
293
294        Ok(HttpRequestResultRaw {
295            status,
296            result_header,
297            content_length,
298            body: Box::new(resp.into_body().into_reader()),
299        })
300    }
301
302    fn new_request(&self, url: &str) -> Self::Request {
303        UreqRequest {
304            req: self.agent.post(url),
305        }
306    }
307}
308
309/// This is an implementation detail of the HTTP client.
310pub struct UreqRequest {
311    req: ureq::RequestBuilder<WithBody>,
312}
313
314impl HttpRequest for UreqRequest {
315    fn set_header(mut self, name: &str, value: &str) -> Self {
316        self.req = self.req.header(name, value);
317        self
318    }
319}
320
321/// Errors from the HTTP client encountered in the course of making a request.
322#[derive(thiserror::Error, Debug)]
323#[allow(clippy::large_enum_variant)] // it's always boxed
324pub enum DefaultClientError {
325    /// The HTTP client encountered invalid UTF-8 data.
326    #[error("invalid UTF-8 string")]
327    Utf8(#[from] std::str::Utf8Error),
328
329    /// The HTTP client encountered some I/O error.
330    #[error("I/O error: {0}")]
331    #[allow(clippy::upper_case_acronyms)]
332    IO(#[from] std::io::Error),
333
334    /// Some other error from the HTTP client implementation.
335    #[error(transparent)]
336    Request(#[from] RequestError),
337}
338
339macro_rules! wrap_error {
340    ($e:ty) => {
341        impl From<$e> for crate::Error {
342            fn from(e: $e) -> Self {
343                Self::HttpClient(Box::new(DefaultClientError::from(e)))
344            }
345        }
346    };
347}
348
349wrap_error!(std::io::Error);
350wrap_error!(std::str::Utf8Error);
351wrap_error!(RequestError);
352
353/// Something went wrong making the request, or the server returned a response we didn't expect.
354/// Use the `Display` or `Debug` impls to see more details.
355/// Note that this type is intentionally vague about the details beyond these string
356/// representations, to allow implementation changes in the future.
357pub struct RequestError {
358    inner: ureq::Error,
359}
360
361impl std::fmt::Display for RequestError {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        <ureq::Error as std::fmt::Display>::fmt(&self.inner, f)
364    }
365}
366
367impl std::fmt::Debug for RequestError {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        <ureq::Error as std::fmt::Debug>::fmt(&self.inner, f)
370    }
371}
372
373impl std::error::Error for RequestError {
374    fn cause(&self) -> Option<&dyn std::error::Error> {
375        Some(&self.inner)
376    }
377}