spirit_reqwest/
lib.rs

1#![doc(test(attr(deny(warnings))))]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6//! This helps with configuring the [`reqwest`] [`Client`].
7//!
8//! This is part of the [`spirit`] system.
9//!
10//! There are two levels of support. The first one is just letting the [`Spirit`] to load the
11//! [`ReqwestClient`] configuration fragment and calling one of its methods to create the
12//! [`Client`] or others.
13//!
14//! The other, more convenient way, is pairing an extractor function with the
15//! [`AtomicClient`][futures::AtomicClient] and
16//! letting [`Spirit`] keep an up to date version of [`Client`] in there at all times.
17//!
18//! # The split and features
19//!
20//! The [`ReqwestClient`] lives at the top of the crate. However, [`reqwest`] provides both
21//! blocking and async flavours of the HTTP client. For that reason, this crate provides two
22//! submodules, each with the relevant support (note that the name of the async one is [`futures`],
23//! because `async` is a keyword). The pipeline is configured with the relevant `IntoClient`
24//! transformation and installed into the relevant `AtomicClient`.
25//!
26//! Features enable parts of the functionality here and correspond to some of the features of
27//! [`reqwest`]. In particular:
28//!
29//! * `gzip`: The `enable-gzip` configuration option.
30//! * `brotli`: The `enable-brotli` configuration option.
31//! * `native-tls`: The `tls-identity`, `tls-identity-password` and `tls-accept-invalid-hostnames`
32//!   options.
33//! * `blocking`: The whole [`blocking`] module and methods for creating the blocking client and
34//!   builder.
35//!
36//! # Porting from the 0.3 version
37//!
38//! * You may need to enable certain features (if you want to keep using the blocking API, you need
39//!   the `blocking` feature, but you also may want the `native-tls` and `gzip` features to get the
40//!   same feature coverage).
41//! * Part of what you used moved to the submodule, but otherwise should have same or similar API
42//! * The pipeline needs the addition of `.transform(IntoClient)` between config extraction and
43//!   installation, to choose if you are interested in blocking or async flavour.
44//!
45//! # Examples
46//!
47//! ```rust
48//! # #[cfg(feature = "blocking")] mod example {
49//! use serde::Deserialize;
50//! use spirit::{Empty, Pipeline, Spirit};
51//! use spirit::prelude::*;
52//! use spirit_reqwest::ReqwestClient;
53//! // Here we choose if we want blocking or async (futures module)
54//! use spirit_reqwest::blocking::{AtomicClient, IntoClient};
55//!
56//! #[derive(Debug, Default, Deserialize)]
57//! struct Cfg {
58//!     #[serde(default)]
59//!     client: ReqwestClient,
60//! }
61//!
62//! impl Cfg {
63//!     fn client(&self) -> ReqwestClient {
64//!         self.client.clone()
65//!     }
66//! }
67//!
68//! # pub
69//! fn main() {
70//!     let client = AtomicClient::unconfigured(); // Get a default config before we get configured
71//!     Spirit::<Empty, Cfg>::new()
72//!         .with(
73//!             Pipeline::new("http client")
74//!                 .extract_cfg(Cfg::client)
75//!                 // Choose if you want blocking or async client
76//!                 // (eg. spirit_reqwest::blocking::IntoClient or
77//!                 // spirit_reqwest::futures::IntoClient)
78//!                 .transform(IntoClient)
79//!                 // Choose where to store it
80//!                 .install(client.clone())
81//!         )
82//!         .run(move |_| {
83//!             let page = client
84//!                 .get("https://www.rust-lang.org")
85//!                 .send()?
86//!                 .error_for_status()?
87//!                 .text()?;
88//!             println!("{}", page);
89//!             Ok(())
90//!         });
91//! }
92//! # }
93//! # #[cfg(not(feature = "blocking"))] mod example { pub fn main() {} }
94//! # fn main() { example::main() }
95//! ```
96//!
97//! [`create`]: ReqwestClient::create
98//! [`builder`]: ReqwestClient::builder
99//! [`Spirit`]: spirit::Spirit
100
101use std::collections::HashMap;
102use std::fs;
103use std::net::IpAddr;
104use std::path::{Path, PathBuf};
105use std::time::Duration;
106
107use err_context::prelude::*;
108use log::{debug, trace, warn};
109#[cfg(feature = "blocking")]
110use reqwest::blocking::{Client as BlockingClient, ClientBuilder as BlockingBuilder};
111use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
112use reqwest::redirect::Policy;
113use reqwest::{Certificate, Client, ClientBuilder, Proxy};
114use serde::{Deserialize, Serialize};
115use spirit::fragment::driver::CacheEq;
116use spirit::utils::is_default;
117#[cfg(feature = "native-tls")]
118use spirit::utils::Hidden;
119use spirit::AnyError;
120use url::Url;
121
122/*
123 * TODO: Logging
124 */
125
126fn load_cert(path: &Path) -> Result<Certificate, AnyError> {
127    let cert = fs::read(path)?;
128    const BEGIN_CERT: &[u8] = b"-----BEGIN CERTIFICATE-----";
129    let contains_begin_cert = cert.windows(BEGIN_CERT.len()).any(|w| w == BEGIN_CERT);
130    let result = if contains_begin_cert {
131        trace!("Loading as PEM");
132        Certificate::from_pem(&cert)?
133    } else {
134        trace!("Loading as DER");
135        Certificate::from_der(&cert)?
136    };
137    Ok(result)
138}
139
140#[cfg(feature = "native-tls")]
141fn load_identity(path: &Path, passwd: &str) -> Result<reqwest::Identity, AnyError> {
142    let identity = fs::read(path)?;
143    Ok(reqwest::Identity::from_pkcs12_der(&identity, passwd)?)
144}
145
146#[allow(clippy::trivially_copy_pass_by_ref)]
147fn is_false(b: &bool) -> bool {
148    !*b
149}
150
151/// A configuration fragment to configure the reqwest [`Client`]
152///
153/// This carries configuration used to build a reqwest [`Client`]. An empty configuration
154/// corresponds to default [`Client::new()`], but most things can be overridden.
155///
156/// The client can be created either manually by methods here, or by pairing it with
157/// [`AtomicClient`][futures::AtomicClient]. See the [crate example](index.html#examples)
158///
159/// # Fields
160///
161/// * `extra-root-certs`: Array of paths, all will be loaded and *added* to the default
162///   certification store. Can be either PEM or DER.
163/// * `tls-identity`: A client identity to use to authenticate to the server. Needs to be a PKCS12
164///   DER bundle. A password might be specified by the `tls-identity-password` field.
165/// * `tls-accept-invalid-hostnames`: If set to true, it accepts invalid hostnames on https.
166///   **Dangerous**, avoid if possible (default is `false`).
167/// * `tls-accept-invalid-certs`: Allow accepting invalid https certificates. **Dangerous**, avoid
168///   if possible (default is `false`).
169/// * `enable-gzip`: Enable gzip compression of transferred data. Default is `true`.
170/// * `enable-brotli`: Enable brotli compression of transferred data. Default is `true`.
171/// * `default-headers`: A bundle of headers a request starts with. Map of name-value, defaults to
172///   empty.
173/// * `user-agent`: The user agent to send with requests.
174/// * `timeout`: Default whole-request timeout. Can be a time specification (with units) or `nil`
175///   for no timeout. Default is `30s`.
176/// * `connect-timeout`: Timeout for the connection phase of a request (with units) or `nil` for no
177///   such timeout. Default is no timeout.
178/// * `max-idle-per-host`: Maximal number of idle connection per one host in the pool. Defaults to
179///   `nil` (no limit).
180/// * `pool-idle-timeout`: How long to keep unused connections around (`nil` to no limit`).
181/// * `http2-only`: Use only HTTP/2. Default is false (both HTTP/1 and HTTP/2 are allowed).
182/// * `http2-initial-stream-window-size`, `http2-initial-connection-window-size`: Tweak the low
183///   level TCP options.
184/// * `http1-case-sensitive-headers`: Consider HTTP/1 headers case sensitive.
185/// * `local-address`: Make the requests from this address. Default is `nil`, which lets the OS to
186///   choose.
187/// * `http-proxy`: An URL of proxy that serves http requests.
188/// * `https-proxy`: An URL of proxy that servers https requests.
189/// * `disable-proxy`: If set to true, disables all use of proxy (including auto-detected system
190///   one).
191/// * `redirects`: Number of allowed redirects per one request, `nil` to disable. Defaults to `10`.
192/// * `referer`: Allow automatic setting of the referer header. Defaults to `true`.
193/// * `tcp-nodelay`: Use the `SO_NODELAY` flag on all connections.
194/// * `tcp-keepalive`: Set the `SO_KEEPALIVE` option to the given time or disable with `nil`.
195#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[cfg_attr(feature = "cfg-help", derive(structdoc::StructDoc))]
197#[serde(rename_all = "kebab-case", default)]
198#[non_exhaustive]
199pub struct ReqwestClient {
200    /// Set the user agent header.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub user_agent: Option<String>,
203
204    /// Timeout for connections sitting unused in the pool.
205    #[serde(
206        deserialize_with = "spirit::utils::deserialize_opt_duration",
207        serialize_with = "spirit::utils::serialize_opt_duration",
208        skip_serializing_if = "Option::is_none"
209    )]
210    pub pool_idle_timeout: Option<Duration>,
211
212    /// Initial HTTP2 window size.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub http2_initial_stream_window_size: Option<u32>,
215
216    /// Initial HTTP2 connection window size.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub http2_initial_connection_window_size: Option<u32>,
219
220    /// Requires that all sockets used have the `SO_NODELAY` set.
221    ///
222    /// This improves latency in some cases at the cost of sending more packets.
223    ///
224    /// On by default.
225    pub tcp_nodelay: bool,
226
227    /// The value of `SO_KEEPALIVE`.
228    ///
229    /// Can be disabled with `nil`.
230    pub tcp_keepalive: Option<Duration>,
231
232    /// Additional certificates to add into the TLS trust store.
233    ///
234    /// Certificates in these files will be considered trusted in addition to the system trust
235    /// store.
236    ///
237    /// Accepts PEM and DER formats (autodetected).
238    #[serde(skip_serializing_if = "Vec::is_empty")]
239    pub tls_extra_root_certs: Vec<PathBuf>,
240
241    /// Client identity.
242    ///
243    /// A file with client certificate and private key that'll be used to authenticate against the
244    /// server. This needs to be a PKCS12 format.
245    ///
246    /// If not set, no client identity is used.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    #[cfg(feature = "native-tls")]
249    pub tls_identity: Option<PathBuf>,
250
251    /// A password for the client identity file.
252    ///
253    /// If tls-identity is not set, the value here is ignored. If not set and the tls-identity is
254    /// present, an empty password is attempted.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    #[cfg(feature = "native-tls")]
257    pub tls_identity_password: Option<Hidden<String>>,
258
259    /// When validating the server certificate, accept even invalid or not matching hostnames.
260    ///
261    /// **DANGEROUS**
262    ///
263    /// Do not set unless you are 100% sure you have to and know what you're doing. This bypasses
264    /// part of the protections TLS provides.
265    ///
266    /// Default is `false` (eg. invalid hostnames are not accepted).
267    #[serde(skip_serializing_if = "is_false")]
268    pub tls_accept_invalid_hostnames: bool,
269
270    /// When validating the server certificate, accept even invalid or untrusted certificates.
271    ///
272    /// **DANGEROUS**
273    ///
274    /// Do not set unless you are 100% sure you have to and know what you're doing. This bypasses
275    /// part of the protections TLS provides.
276    ///
277    /// Default is `false` (eg. invalid certificates are not accepted).
278    #[serde(skip_serializing_if = "is_false")]
279    pub tls_accept_invalid_certs: bool,
280
281    /// Enables gzip transport compression.
282    ///
283    /// Default is on.
284    #[cfg(feature = "gzip")]
285    pub enable_gzip: bool,
286
287    /// Enables brotli transport compression.
288    ///
289    /// Default is on.
290    #[cfg(feature = "brotli")]
291    pub enable_brotli: bool,
292
293    /// Headers added to each request.
294    ///
295    /// This can be used for example to add `User-Agent` header.
296    ///
297    /// By default no headers are added.
298    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
299    pub default_headers: HashMap<String, String>,
300
301    /// A whole-request timeout.
302    ///
303    /// If the request doesn't happen during this time, it gives up.
304    ///
305    /// The default is `30s`. Can be turned off by setting to `nil`.
306    #[serde(
307        deserialize_with = "spirit::utils::deserialize_opt_duration",
308        serialize_with = "spirit::utils::serialize_opt_duration"
309    )]
310    pub timeout: Option<Duration>,
311
312    /// A timeout for connecting to the server.
313    ///
314    /// The default is no connection timeout.
315    #[serde(
316        deserialize_with = "spirit::utils::deserialize_opt_duration",
317        serialize_with = "spirit::utils::serialize_opt_duration"
318    )]
319    pub connect_timeout: Option<Duration>,
320
321    /// An URL for proxy to use on HTTP requests.
322    ///
323    /// No proxy is used if not set.
324    #[structdoc(leaf = "URL")]
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub http_proxy: Option<Url>,
327
328    /// An URL for proxy to use on HTTPS requests.
329    ///
330    /// No proxy is used if not set.
331    #[structdoc(leaf = "URL")]
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub https_proxy: Option<Url>,
334
335    /// Disable use of all proxies.
336    ///
337    /// This disables both the proxies configured here and proxy auto-detected from system.
338    /// Overrides both `http_proxy` and `https_proxy`.
339    #[serde(default, skip_serializing_if = "is_default")]
340    pub disable_proxy: bool,
341
342    /// How many redirects to allow for one request.
343    ///
344    /// The default value is 10. Support for redirects can be completely disabled by setting this
345    /// to `nil`.
346    pub redirects: Option<usize>,
347
348    /// Manages automatic setting of the Referer header.
349    ///
350    /// Default is on.
351    pub referer: bool,
352
353    /// Maximum number of idle connections per one host.
354    ///
355    /// Default is no limit.
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub max_idle_per_host: Option<usize>,
358
359    /// Use only HTTP/2.
360    ///
361    /// Default is false.
362    #[serde(default)]
363    pub http2_only: bool,
364
365    /// Use HTTP/1 headers in case sensitive manner.
366    #[serde(default)]
367    pub http1_case_sensitive_headers: bool,
368
369    /// The local address connections are made from.
370    ///
371    /// Default is no address (the OS will choose).
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub local_address: Option<IpAddr>,
374
375    /// Restrict to using only https.
376    ///
377    /// Default is false.
378    pub https_only: bool,
379}
380
381impl Default for ReqwestClient {
382    fn default() -> Self {
383        ReqwestClient {
384            tls_extra_root_certs: Vec::new(),
385            #[cfg(feature = "native-tls")]
386            tls_identity: None,
387            #[cfg(feature = "native-tls")]
388            tls_identity_password: None,
389            tls_accept_invalid_hostnames: false,
390            tls_accept_invalid_certs: false,
391            #[cfg(feature = "gzip")]
392            enable_gzip: true,
393            #[cfg(feature = "brotli")]
394            enable_brotli: true,
395            default_headers: HashMap::new(),
396            user_agent: None,
397            timeout: Some(Duration::from_secs(30)),
398            connect_timeout: None,
399            pool_idle_timeout: Some(Duration::from_secs(90)),
400            http_proxy: None,
401            https_proxy: None,
402            disable_proxy: false,
403            redirects: Some(10),
404            referer: true,
405            http2_only: false,
406            http1_case_sensitive_headers: false,
407            http2_initial_connection_window_size: None,
408            http2_initial_stream_window_size: None,
409            max_idle_per_host: None,
410            tcp_nodelay: false,
411            tcp_keepalive: None,
412            local_address: None,
413            https_only: false,
414        }
415    }
416}
417
418impl ReqwestClient {
419    /// Creates a pre-configured [`ClientBuilder`]
420    ///
421    /// This configures everything according to `self` and then returns the builder. The caller can
422    /// modify it further and then create the client.
423    ///
424    /// Unless there's a need to tweak the configuration, the [`create_async_client`] is more
425    /// comfortable.
426    ///
427    /// [`create_async_client`]: ReqwestClient::create_async_client
428    pub fn async_builder(&self) -> Result<ClientBuilder, AnyError> {
429        debug!("Creating Reqwest client from {:?}", self);
430        let mut headers = HeaderMap::new();
431        for (key, val) in &self.default_headers {
432            let name = HeaderName::from_bytes(key.as_bytes())
433                .with_context(|_| format!("{} is not a valiad header name", key))?;
434            let header = HeaderValue::from_bytes(val.as_bytes())
435                .with_context(|_| format!("{} is not a valid header", val))?;
436            headers.insert(name, header);
437        }
438        let redirects = match self.redirects {
439            None => Policy::none(),
440            Some(limit) => Policy::limited(limit),
441        };
442        let mut builder = Client::builder()
443            .danger_accept_invalid_certs(self.tls_accept_invalid_certs)
444            .tcp_nodelay(self.tcp_nodelay)
445            .tcp_keepalive(self.tcp_keepalive)
446            .pool_max_idle_per_host(self.max_idle_per_host.unwrap_or(usize::max_value()))
447            .pool_idle_timeout(self.pool_idle_timeout)
448            .local_address(self.local_address)
449            .default_headers(headers)
450            .redirect(redirects)
451            .referer(self.referer)
452            .https_only(self.https_only);
453        #[cfg(feature = "gzip")]
454        {
455            builder = builder.gzip(self.enable_gzip);
456        }
457        #[cfg(feature = "brotli")]
458        {
459            builder = builder.brotli(self.enable_brotli);
460        }
461        #[cfg(feature = "native-tls")]
462        {
463            builder = builder.danger_accept_invalid_hostnames(self.tls_accept_invalid_hostnames);
464        }
465        if let Some(agent) = self.user_agent.as_ref() {
466            builder = builder.user_agent(agent);
467        }
468        if let Some(timeout) = self.timeout {
469            builder = builder.timeout(timeout);
470        }
471        if let Some(connect_timeout) = self.connect_timeout {
472            builder = builder.connect_timeout(connect_timeout);
473        }
474        if self.http2_only {
475            builder = builder.http2_prior_knowledge();
476        }
477        if self.http1_case_sensitive_headers {
478            builder = builder.http1_title_case_headers();
479        }
480        for cert_path in &self.tls_extra_root_certs {
481            trace!("Adding root certificate {:?}", cert_path);
482            let cert = load_cert(cert_path)
483                .with_context(|_| format!("Failed to load certificate {:?}", cert_path))?;
484            builder = builder.add_root_certificate(cert);
485        }
486        #[cfg(feature = "native-tls")]
487        if let Some(identity_path) = &self.tls_identity {
488            trace!("Setting TLS client identity {:?}", identity_path);
489            let passwd: &str = self
490                .tls_identity_password
491                .as_ref()
492                .map(|s| s as &str)
493                .unwrap_or_default();
494            let identity = load_identity(&identity_path, passwd)
495                .with_context(|_| format!("Failed to load identity {:?}", identity_path))?;
496            builder = builder.identity(identity);
497        }
498        if self.disable_proxy {
499            builder = builder.no_proxy();
500            if self.http_proxy.is_some() || self.https_proxy.is_some() {
501                warn!("disable-proxy overrides manually set proxy");
502            }
503        } else {
504            if let Some(proxy) = &self.http_proxy {
505                let proxy_url = proxy.clone();
506                let proxy = Proxy::http(proxy_url)
507                    .with_context(|_| format!("Failed to configure http proxy to {:?}", proxy))?;
508                builder = builder.proxy(proxy);
509            }
510            if let Some(proxy) = &self.https_proxy {
511                let proxy_url = proxy.clone();
512                let proxy = Proxy::https(proxy_url)
513                    .with_context(|_| format!("Failed to configure https proxy to {:?}", proxy))?;
514                builder = builder.proxy(proxy);
515            }
516        }
517
518        Ok(builder)
519    }
520
521    /// Creates a blocking [`ClientBuilder`][BlockingBuilder].
522    #[cfg(feature = "blocking")]
523    pub fn blocking_builder(&self) -> Result<BlockingBuilder, AnyError> {
524        self.async_builder()
525            .map(BlockingBuilder::from)
526            // It seems the blocking builder does not preserve the timeout. A bug there?
527            .map(|builder| builder.timeout(self.timeout))
528    }
529
530    /// Creates a [`Client`][BlockingClient] according to the configuration inside `self`.
531    ///
532    /// This is for manually creating the client. It is also possible to pair with an
533    /// [`AtomicClient`][blocking::AtomicClient] to form a
534    /// [`Pipeline`][spirit::fragment::pipeline::Pipeline].
535    #[cfg(feature = "blocking")]
536    pub fn create_blocking_client(&self) -> Result<BlockingClient, AnyError> {
537        self.blocking_builder()?
538            .build()
539            .context("Failed to finish creating Reqwest HTTP client")
540            .map_err(AnyError::from)
541    }
542
543    /// Creates a [`Client`] according to the configuration inside `self`.
544    ///
545    /// This is for manually creating the client. It is also possible to pair with an
546    /// [`AtomicClient`][futures::AtomicClient] to form a
547    /// [`Pipeline`][spirit::fragment::pipeline::Pipeline].
548    pub fn create_async_client(&self) -> Result<Client, AnyError> {
549        self.async_builder()?
550            .build()
551            .context("Failed to finish creating Reqwest HTTP client")
552            .map_err(AnyError::from)
553    }
554}
555
556spirit::simple_fragment! {
557    impl Fragment for ReqwestClient {
558        type Driver = CacheEq<ReqwestClient>;
559        type Resource = ClientBuilder;
560        type Installer = ();
561        fn create(&self, _: &'static str) -> Result<ClientBuilder, AnyError> {
562            self.async_builder()
563        }
564    }
565}
566
567macro_rules! method {
568    ($($(#[$attr: meta])* $name: ident();)*) => {
569        $(
570            $(#[$attr])*
571            pub fn $name<U: IntoUrl>(&self, url: U) -> RequestBuilder {
572                self.0
573                    .load()
574                    .as_ref()
575                    .expect("Accessing Reqwest HTTP client before setting it up")
576                    .$name(url)
577            }
578        )*
579    }
580}
581
582macro_rules! submodule {
583($(#[$attr: meta])* pub mod $module:ident with $path:path) => {
584$(#[$attr])*
585pub mod $module {
586    use std::sync::Arc;
587
588    use arc_swap::ArcSwapOption;
589    use err_context::AnyError;
590    use err_context::prelude::*;
591    use log::debug;
592    use $path::{Client, ClientBuilder, RequestBuilder};
593    use reqwest::{IntoUrl, Method};
594    use spirit::fragment::{Installer, Transformation};
595
596    /// A storage for one [`Client`] that can be atomically exchanged under the hood.
597    ///
598    /// This acts as a proxy for a [`Client`]. This is cheap to clone all cloned handles refer to
599    /// the same client. It has most of the [`Client`]'s methods directly on itself, the others can
600    /// be accessed through the [`client`] method.
601    ///
602    /// It also supports the [`replace`] method, by which it is possible to exchange the client
603    /// inside.
604    ///
605    /// While it can be used separately, it is best paired with a
606    /// [`ReqwestClient`][crate::ReqwestClient] configuration fragment inside [`Spirit`] to have an
607    /// up to date client around.
608    ///
609    /// # Warning
610    ///
611    /// As it is possible for the client to get replaced at any time by another thread, therefore
612    /// successive calls to eg. [`get`] may happen on different clients. If this is a problem, a
613    /// caller may get a specific client by the [`client`] method ‒ the client returned will not
614    /// change for as long as it is held (if the one inside here is replaced, both are kept alive
615    /// until the return value of [`client`] goes out of scope).
616    ///
617    /// # Panics
618    ///
619    /// Trying to access the client if the [`AtomicClient`] was created with [`empty`] and wasn't
620    /// set yet (either by [`Spirit`] or by explicit [`replace`]) will result into panic.
621    ///
622    /// If you may use the client sooner, prefer either `default` or [`unconfigured`].
623    ///
624    /// [`unconfigured`]: AtomicClient::unconfigured
625    /// [`Spirit`]: spirit::Spirit
626    /// [`replace`]: AtomicClient::replace
627    /// [`empty`]: AtomicClient::empty
628    /// [`client`]: AtomicClient::client
629    /// [`get`]: AtomicClient::get
630    #[derive(Clone, Debug)]
631    pub struct AtomicClient(Arc<ArcSwapOption<Client>>);
632
633    impl Default for AtomicClient {
634        fn default() -> Self {
635            Self::unconfigured()
636        }
637    }
638
639    impl<C: Into<Arc<Client>>> From<C> for AtomicClient {
640        fn from(c: C) -> Self {
641            AtomicClient(Arc::new(ArcSwapOption::from(Some(c.into()))))
642        }
643    }
644
645    impl AtomicClient {
646        /// Creates an empty [`AtomicClient`].
647        ///
648        /// This is effectively a `NULL`. It'll panic until a value is set, either by [`replace`]
649        /// or by [`Spirit`] behind the scenes. It is appropriate if the caller is sure it will get
650        /// configured before being accessed and creating an intermediate client first would be a
651        /// waste.
652        ///
653        /// [`replace`]: AtomicClient::replace
654        /// [`Spirit`]: spirit::Spirit
655        pub fn empty() -> Self {
656            AtomicClient(Arc::new(ArcSwapOption::empty()))
657        }
658
659        /// Creates an [`AtomicClient`] with default [`Client`] inside.
660        pub fn unconfigured() -> Self {
661            AtomicClient(Arc::new(ArcSwapOption::from_pointee(Client::new())))
662        }
663
664        /// Replaces the content of this [`AtomicClient`] with a new [`Client`].
665        ///
666        /// If you want to create a new [`AtomicClient`] out of a client, use [`From`]. This is
667        /// meant for replacing the content of already existing ones.
668        ///
669        /// This replaces it for *all* connected handles (eg. created by cloning from the same
670        /// original [`AtomicClient`]).
671        pub fn replace<C: Into<Arc<Client>>>(&self, by: C) {
672            let client = by.into();
673            self.0.store(Some(client));
674        }
675
676        /// Returns a handle to the [`Client`] currently held inside.
677        ///
678        /// This serves a dual purpose:
679        ///
680        /// * If some functionality is not directly provided by the [`AtomicClient`] proxy.
681        /// * If the caller needs to ensure a series of requests is performed using the same client.
682        ///   While the content of the [`AtomicClient`] can change between calls to it, the content of
683        ///   the [`Arc`] can't. While it is possible the client inside [`AtomicClient`] exchanged, the
684        ///   [`Arc`] keeps its [`Client`] around (which may lead to multiple [`Client`]s in memory).
685        pub fn client(&self) -> Arc<Client> {
686            self.0
687                .load_full()
688                .expect("Accessing Reqwest HTTP client before setting it up")
689        }
690
691        /// Starts building an arbitrary request using the current client.
692        ///
693        /// This is forwarded to [`Client::request`].
694        pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
695            self.0
696                .load()
697                .as_ref()
698                .expect("Accessing Reqwest HTTP client before setting it up")
699                .request(method, url)
700        }
701        method! {
702            /// Starts building a GET request.
703            ///
704            /// This is forwarded to [`Client::get`].
705            get();
706
707            /// Starts building a POST request.
708            ///
709            /// This is forwarded to [`Client::post`].
710            post();
711
712            /// Starts building a PUT request.
713            ///
714            /// This is forwarded to [`Client::put`].
715            put();
716
717            /// Starts building a PATCH request.
718            ///
719            /// This is forwarded to [`Client::patch`].
720            patch();
721
722            /// Starts building a DELETE request.
723            ///
724            /// This is forwarded to [`Client::delete`].
725            delete();
726
727            /// Starts building a HEAD request.
728            ///
729            /// This is forwarded to [`Client::head`].
730            head();
731        }
732    }
733
734    /// A transformation to turn a [`ClientBuilder`] into a [`Client`].
735    ///
736    /// To be used inside a [`Pipeline`][spirit::fragment::pipeline::Pipeline].
737    pub struct IntoClient;
738
739    impl<I, F> Transformation<reqwest::ClientBuilder, I, F> for IntoClient {
740        type OutputResource = Client;
741        type OutputInstaller = ();
742        fn installer(&mut self, _: I, _: &str) {}
743        fn transform(
744            &mut self,
745            builder: reqwest::ClientBuilder,
746            _: &F,
747            _: &str,
748            ) -> Result<Self::OutputResource, AnyError> {
749            let builder = ClientBuilder::from(builder);
750            builder
751                .build()
752                .context("Failed to finish creating Reqwest HTTP client")
753                .map_err(AnyError::from)
754        }
755    }
756
757    impl<O, C> Installer<Client, O, C> for AtomicClient {
758        type UninstallHandle = ();
759        fn install(&mut self, client: Client, name: &'static str) {
760            debug!("Installing http client '{}'", name);
761            self.replace(client);
762        }
763    }
764}
765}
766}
767
768#[cfg(feature = "blocking")]
769submodule! {
770/// The support for blocking clients.
771pub mod blocking with reqwest::blocking
772}
773
774submodule! {
775/// The support for async clients.
776pub mod futures with reqwest
777}