graph_http/
client.rs

1use crate::blocking::BlockingClient;
2use graph_core::identity::{ClientApplication, ForceTokenRefresh};
3use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT};
4use reqwest::redirect::Policy;
5use reqwest::tls::Version;
6use reqwest::Proxy;
7use reqwest::{Request, Response};
8use std::env::VarError;
9use std::ffi::OsStr;
10use std::fmt::{Debug, Formatter};
11use std::time::Duration;
12use tower::limit::ConcurrencyLimitLayer;
13use tower::retry::RetryLayer;
14use tower::util::BoxCloneService;
15use tower::ServiceExt;
16
17fn user_agent_header_from_env() -> Option<HeaderValue> {
18    let header = std::option_env!("GRAPH_CLIENT_USER_AGENT")?;
19    HeaderValue::from_str(header).ok()
20}
21
22#[derive(Default, Clone)]
23struct ServiceLayersConfiguration {
24    concurrency_limit: Option<usize>,
25    retry: Option<usize>,
26    wait_for_retry_after_headers: Option<()>,
27}
28
29#[derive(Clone)]
30struct ClientConfiguration {
31    client_application: Option<Box<dyn ClientApplication>>,
32    headers: HeaderMap,
33    referer: bool,
34    timeout: Option<Duration>,
35    connect_timeout: Option<Duration>,
36    connection_verbose: bool,
37    https_only: bool,
38    /// TLS 1.2 required to support all features in Microsoft Graph
39    /// See [Reliability and Support](https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support)
40    min_tls_version: Version,
41    service_layers_configuration: ServiceLayersConfiguration,
42    proxy: Option<Proxy>,
43}
44
45impl ClientConfiguration {
46    pub fn new() -> ClientConfiguration {
47        let mut headers: HeaderMap<HeaderValue> = HeaderMap::with_capacity(2);
48        headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
49
50        if let Some(user_agent) = user_agent_header_from_env() {
51            headers.insert(USER_AGENT, user_agent);
52        }
53
54        ClientConfiguration {
55            client_application: None,
56            headers,
57            referer: true,
58            timeout: None,
59            connect_timeout: None,
60            connection_verbose: false,
61            https_only: true,
62            min_tls_version: Version::TLS_1_2,
63            service_layers_configuration: ServiceLayersConfiguration::default(),
64            proxy: None,
65        }
66    }
67}
68
69impl PartialEq for ClientConfiguration {
70    fn eq(&self, other: &Self) -> bool {
71        self.headers == other.headers
72            && self.referer == other.referer
73            && self.connect_timeout == other.connect_timeout
74            && self.connection_verbose == other.connection_verbose
75            && self.https_only == other.https_only
76            && self.min_tls_version == other.min_tls_version
77    }
78}
79
80impl Debug for ClientConfiguration {
81    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("ClientConfiguration")
83            .field("headers", &self.headers)
84            .field("referer", &self.referer)
85            .field("timeout", &self.timeout)
86            .field("connect_timeout", &self.connect_timeout)
87            .field("https_only", &self.https_only)
88            .field("min_tls_version", &self.min_tls_version)
89            .field("proxy", &self.proxy)
90            .finish()
91    }
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub struct GraphClientConfiguration {
96    config: ClientConfiguration,
97}
98
99impl GraphClientConfiguration {
100    pub fn new() -> GraphClientConfiguration {
101        GraphClientConfiguration {
102            config: ClientConfiguration::new(),
103        }
104    }
105
106    pub fn access_token<AT: ToString>(mut self, access_token: AT) -> GraphClientConfiguration {
107        self.config.client_application = Some(Box::new(access_token.to_string()));
108        self
109    }
110
111    pub fn client_application<CA: ClientApplication + 'static>(mut self, client_app: CA) -> Self {
112        self.config.client_application = Some(Box::new(client_app));
113        self
114    }
115
116    pub fn default_headers(mut self, headers: HeaderMap) -> GraphClientConfiguration {
117        for (key, value) in headers.iter() {
118            self.config.headers.insert(key, value.clone());
119        }
120        self
121    }
122
123    /// Enable or disable automatic setting of the `Referer` header.
124    ///
125    /// Default is `true`.
126    pub fn referer(mut self, enable: bool) -> GraphClientConfiguration {
127        self.config.referer = enable;
128        self
129    }
130
131    /// Enables a request timeout.
132    ///
133    /// The timeout is applied from when the request starts connecting until the
134    /// response body has finished.
135    ///
136    /// Default is no timeout.
137    pub fn timeout(mut self, timeout: Duration) -> GraphClientConfiguration {
138        self.config.timeout = Some(timeout);
139        self
140    }
141
142    /// Set a timeout for only the connect phase of a `Client`.
143    ///
144    /// Default is `None`.
145    ///
146    /// # Note
147    ///
148    /// This **requires** the futures be executed in a tokio runtime with
149    /// a tokio timer enabled.
150    pub fn connect_timeout(mut self, timeout: Duration) -> GraphClientConfiguration {
151        self.config.connect_timeout = Some(timeout);
152        self
153    }
154
155    /// Set whether connections should emit verbose logs.
156    ///
157    /// Enabling this option will emit [log][] messages at the `TRACE` level
158    /// for read and write operations on connections.
159    ///
160    /// [log]: https://crates.io/crates/log
161    pub fn connection_verbose(mut self, verbose: bool) -> GraphClientConfiguration {
162        self.config.connection_verbose = verbose;
163        self
164    }
165
166    pub fn user_agent(mut self, value: HeaderValue) -> GraphClientConfiguration {
167        self.config.headers.insert(USER_AGENT, value);
168        self
169    }
170
171    /// TLS 1.2 required to support all features in Microsoft Graph
172    /// See [Reliability and Support](https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support)
173    pub fn min_tls_version(mut self, version: Version) -> GraphClientConfiguration {
174        self.config.min_tls_version = version;
175        self
176    }
177
178    /// Set [`Proxy`] for all network operations.
179    ///
180    /// Default is no proxy.
181    pub fn proxy(mut self, proxy: Proxy) -> GraphClientConfiguration {
182        self.config.proxy = Some(proxy);
183        self
184    }
185
186    #[cfg(feature = "test-util")]
187    pub fn https_only(mut self, https_only: bool) -> GraphClientConfiguration {
188        self.config.https_only = https_only;
189        self
190    }
191
192    /// Enable a request retry for a failed request. The retry parameter can be used to
193    /// change how many times the request should be retried.
194    ///
195    /// Some requests may fail on GraphAPI side and should be retried.
196    /// Only server errors (HTTP code between 500 and 599) will be retried.
197    ///
198    /// Default is no retry.
199    pub fn retry(mut self, retry: Option<usize>) -> GraphClientConfiguration {
200        self.config.service_layers_configuration.retry = retry;
201        self
202    }
203
204    /// Enable a request retry if we reach the throttling limits and GraphAPI returns a
205    /// 429 Too Many Requests with a Retry-After header
206    ///
207    /// Retry attempts are executed when the response has a status code of 429, 500, 503, 504
208    /// and the response has a Retry-After header. The Retry-After header provides a back-off
209    /// time to wait for before retrying the request again.
210    ///
211    /// Be careful with this parameter as some API endpoints have quite
212    /// low limits (reports for example) and the request may hang for hundreds of seconds.
213    /// For maximum throughput you may want to not respect the Retry-After header as hitting
214    /// another server thanks to load-balancing may lead to a successful response.
215    ///
216    /// Default is no retry.
217    pub fn wait_for_retry_after_headers(mut self, retry: bool) -> GraphClientConfiguration {
218        self.config
219            .service_layers_configuration
220            .wait_for_retry_after_headers = match retry {
221            true => Some(()),
222            false => None,
223        };
224        self
225    }
226
227    /// Enable a concurrency limit on the client.
228    ///
229    /// Every request through this client will be subject to a concurrency limit.
230    /// Can be useful to stay under the API limits set by GraphAPI.
231    ///
232    /// Default is no concurrency limit.
233    pub fn concurrency_limit(
234        mut self,
235        concurrency_limit: Option<usize>,
236    ) -> GraphClientConfiguration {
237        self.config.service_layers_configuration.concurrency_limit = concurrency_limit;
238        self
239    }
240
241    pub(crate) fn build_tower_service(
242        &self,
243        client: &reqwest::Client,
244    ) -> BoxCloneService<Request, Response, Box<dyn std::error::Error + Send + Sync>> {
245        tower::ServiceBuilder::new()
246            .option_layer(
247                self.config
248                    .service_layers_configuration
249                    .retry
250                    .map(|num| RetryLayer::new(crate::tower_services::Attempts(num))),
251            )
252            .option_layer(
253                self.config
254                    .service_layers_configuration
255                    .wait_for_retry_after_headers
256                    .map(|_| RetryLayer::new(crate::tower_services::WaitFor())),
257            )
258            .option_layer(
259                self.config
260                    .service_layers_configuration
261                    .concurrency_limit
262                    .map(ConcurrencyLimitLayer::new),
263            )
264            .service(client.clone())
265            .boxed_clone()
266    }
267
268    fn build_http_client(&self) -> reqwest::Client {
269        let headers = self.config.headers.clone();
270        let mut builder = reqwest::ClientBuilder::new()
271            .referer(self.config.referer)
272            .connection_verbose(self.config.connection_verbose)
273            .https_only(self.config.https_only)
274            .min_tls_version(self.config.min_tls_version)
275            .redirect(Policy::limited(2))
276            .default_headers(headers);
277
278        if let Some(timeout) = self.config.timeout {
279            builder = builder.timeout(timeout);
280        }
281
282        if let Some(connect_timeout) = self.config.connect_timeout {
283            builder = builder.connect_timeout(connect_timeout);
284        }
285
286        if let Some(proxy) = &self.config.proxy {
287            builder = builder.proxy(proxy.clone());
288        }
289
290        builder.build().unwrap()
291    }
292
293    fn build_blocking_http_client(&self) -> reqwest::blocking::Client {
294        let headers = self.config.headers.clone();
295        let mut builder = reqwest::blocking::ClientBuilder::new()
296            .referer(self.config.referer)
297            .connection_verbose(self.config.connection_verbose)
298            .https_only(self.config.https_only)
299            .min_tls_version(self.config.min_tls_version)
300            .redirect(Policy::limited(2))
301            .default_headers(headers);
302
303        if let Some(timeout) = self.config.timeout {
304            builder = builder.timeout(timeout);
305        }
306
307        if let Some(connect_timeout) = self.config.connect_timeout {
308            builder = builder.connect_timeout(connect_timeout);
309        }
310
311        if let Some(proxy) = &self.config.proxy {
312            builder = builder.proxy(proxy.clone());
313        }
314
315        builder.build().unwrap()
316    }
317
318    pub(crate) fn build(self) -> Client {
319        let config = self.clone();
320        let headers = self.config.headers.clone();
321        let client = self.build_http_client();
322
323        if let Some(client_application) = self.config.client_application {
324            Client {
325                client_application,
326                inner: client,
327                headers,
328                builder: config,
329            }
330        } else {
331            Client {
332                client_application: Box::<String>::default(),
333                inner: client,
334                headers,
335                builder: config,
336            }
337        }
338    }
339
340    pub(crate) fn build_blocking(self) -> BlockingClient {
341        let headers = self.config.headers.clone();
342        let client = self.build_blocking_http_client();
343
344        if let Some(client_application) = self.config.client_application {
345            BlockingClient {
346                client_application,
347                inner: client,
348                headers,
349            }
350        } else {
351            BlockingClient {
352                client_application: Box::<String>::default(),
353                inner: client,
354                headers,
355            }
356        }
357    }
358
359    pub(crate) fn build_minimal_async_client(self) -> MinimalAsyncClient {
360        let config = self.clone();
361        let client = self.build_http_client();
362        let service = self.build_tower_service(&client);
363        MinimalAsyncClient {
364            inner: client,
365            builder: config,
366            service,
367        }
368    }
369
370    pub(crate) fn build_minimal_blocking_client(self) -> MinimalBlockingClient {
371        let config = self.clone();
372        let client = self.build_blocking();
373        MinimalBlockingClient {
374            inner: client.inner,
375            builder: config,
376        }
377    }
378}
379
380impl Default for GraphClientConfiguration {
381    fn default() -> Self {
382        GraphClientConfiguration::new()
383    }
384}
385
386#[derive(Clone)]
387pub struct Client {
388    pub(crate) client_application: Box<dyn ClientApplication>,
389    pub(crate) inner: reqwest::Client,
390    pub(crate) headers: HeaderMap,
391    pub(crate) builder: GraphClientConfiguration,
392}
393
394impl Client {
395    pub fn new<CA: ClientApplication + 'static>(client_app: CA) -> Self {
396        GraphClientConfiguration::new()
397            .client_application(client_app)
398            .build()
399    }
400
401    pub fn from_access_token<T: AsRef<str>>(access_token: T) -> Self {
402        GraphClientConfiguration::new()
403            .access_token(access_token.as_ref())
404            .build()
405    }
406
407    /// Create a new client and use the given environment variable
408    /// for the access token.
409    pub fn new_env<K: AsRef<OsStr>>(env_var: K) -> Result<Client, VarError> {
410        Ok(GraphClientConfiguration::new()
411            .access_token(std::env::var(env_var)?)
412            .build())
413    }
414
415    pub fn builder() -> GraphClientConfiguration {
416        GraphClientConfiguration::new()
417    }
418
419    pub fn headers(&self) -> &HeaderMap {
420        &self.headers
421    }
422
423    pub fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
424        self.client_application
425            .with_force_token_refresh(force_token_refresh);
426    }
427}
428
429impl Default for Client {
430    fn default() -> Self {
431        GraphClientConfiguration::new().build()
432    }
433}
434
435impl Debug for Client {
436    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
437        f.debug_struct("Client")
438            .field("inner", &self.inner)
439            .field("headers", &self.headers)
440            .field("builder", &self.builder)
441            .finish()
442    }
443}
444
445impl From<GraphClientConfiguration> for Client {
446    fn from(value: GraphClientConfiguration) -> Self {
447        value.build()
448    }
449}
450
451#[derive(Clone)]
452pub struct MinimalAsyncClient {
453    pub inner: reqwest::Client,
454    pub builder: GraphClientConfiguration,
455    pub service: BoxCloneService<Request, Response, Box<dyn std::error::Error + Send + Sync>>,
456}
457
458/*
459let service = inner.builder.build_tower_service(&inner.inner);
460 */
461
462impl From<GraphClientConfiguration> for MinimalAsyncClient {
463    fn from(value: GraphClientConfiguration) -> Self {
464        value.build_minimal_async_client()
465    }
466}
467
468impl Default for MinimalAsyncClient {
469    fn default() -> Self {
470        GraphClientConfiguration::new().build_minimal_async_client()
471    }
472}
473
474#[derive(Clone)]
475pub struct MinimalBlockingClient {
476    pub inner: reqwest::blocking::Client,
477    pub builder: GraphClientConfiguration,
478}
479
480impl From<GraphClientConfiguration> for MinimalBlockingClient {
481    fn from(value: GraphClientConfiguration) -> Self {
482        value.build_minimal_blocking_client()
483    }
484}
485
486impl Default for MinimalBlockingClient {
487    fn default() -> Self {
488        GraphClientConfiguration::new().build_minimal_blocking_client()
489    }
490}
491
492#[cfg(test)]
493mod test {
494    use super::*;
495
496    #[test]
497    fn compile_time_user_agent_header() {
498        let client = GraphClientConfiguration::new()
499            .access_token("access_token")
500            .build();
501
502        assert!(client.builder.config.headers.contains_key(USER_AGENT));
503    }
504
505    #[test]
506    fn update_user_agent_header() {
507        let client = GraphClientConfiguration::new()
508            .access_token("access_token")
509            .user_agent(HeaderValue::from_static("user_agent"))
510            .build();
511
512        assert!(client.builder.config.headers.contains_key(USER_AGENT));
513        let user_agent_header = client.builder.config.headers.get(USER_AGENT).unwrap();
514        assert_eq!("user_agent", user_agent_header.to_str().unwrap());
515    }
516}