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}