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