Skip to main content

olai_http/
client.rs

1use std::str::FromStr;
2use std::time::Duration;
3
4use reqwest::header::{HeaderMap, HeaderValue};
5use reqwest::{Client, ClientBuilder, NoProxy, Proxy};
6use serde::{Deserialize, Serialize};
7
8use crate::config::*;
9use crate::error::{Error, Result};
10
11static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
12
13fn map_client_error(e: reqwest::Error) -> Error {
14    Error::Generic {
15        source: Box::new(e),
16    }
17}
18
19/// Configuration keys for [`ClientOptions`]
20#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
21#[non_exhaustive]
22pub enum ClientConfigKey {
23    /// Allow non-TLS, i.e. non-HTTPS connections
24    AllowHttp,
25    /// Skip certificate validation on https connections.
26    ///
27    /// # Warning
28    ///
29    /// You should think very carefully before using this method. If
30    /// invalid certificates are trusted, *any* certificate for *any* site
31    /// will be trusted for use. This includes expired certificates. This
32    /// introduces significant vulnerabilities, and should only be used
33    /// as a last resort or for testing
34    AllowInvalidCertificates,
35    /// Timeout for only the connect phase of a Client
36    ConnectTimeout,
37    /// Only use http1 connections
38    Http1Only,
39    /// Interval for HTTP2 Ping frames should be sent to keep a connection alive.
40    Http2KeepAliveInterval,
41    /// Timeout for receiving an acknowledgement of the keep-alive ping.
42    Http2KeepAliveTimeout,
43    /// Enable HTTP2 keep alive pings for idle connections
44    Http2KeepAliveWhileIdle,
45    /// Sets the maximum frame size to use for HTTP2.
46    Http2MaxFrameSize,
47    /// Only use http2 connections
48    Http2Only,
49    /// The pool max idle timeout
50    ///
51    /// This is the length of time an idle connection will be kept alive
52    PoolIdleTimeout,
53    /// maximum number of idle connections per host
54    PoolMaxIdlePerHost,
55    /// HTTP proxy to use for requests
56    ProxyUrl,
57    /// PEM-formatted CA certificate for proxy connections
58    ProxyCaCertificate,
59    /// List of hosts that bypass proxy
60    ProxyExcludes,
61    /// Request timeout
62    ///
63    /// The timeout is applied from when the request starts connecting until the
64    /// response body has finished
65    Timeout,
66    /// User-Agent header to be used by this client
67    UserAgent,
68}
69
70impl AsRef<str> for ClientConfigKey {
71    fn as_ref(&self) -> &str {
72        match self {
73            Self::AllowHttp => "allow_http",
74            Self::AllowInvalidCertificates => "allow_invalid_certificates",
75            Self::ConnectTimeout => "connect_timeout",
76            Self::Http1Only => "http1_only",
77            Self::Http2Only => "http2_only",
78            Self::Http2KeepAliveInterval => "http2_keep_alive_interval",
79            Self::Http2KeepAliveTimeout => "http2_keep_alive_timeout",
80            Self::Http2KeepAliveWhileIdle => "http2_keep_alive_while_idle",
81            Self::Http2MaxFrameSize => "http2_max_frame_size",
82            Self::PoolIdleTimeout => "pool_idle_timeout",
83            Self::PoolMaxIdlePerHost => "pool_max_idle_per_host",
84            Self::ProxyUrl => "proxy_url",
85            Self::ProxyCaCertificate => "proxy_ca_certificate",
86            Self::ProxyExcludes => "proxy_excludes",
87            Self::Timeout => "timeout",
88            Self::UserAgent => "user_agent",
89        }
90    }
91}
92
93impl FromStr for ClientConfigKey {
94    type Err = Error;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        match s {
98            "allow_http" => Ok(Self::AllowHttp),
99            "allow_invalid_certificates" => Ok(Self::AllowInvalidCertificates),
100            "connect_timeout" => Ok(Self::ConnectTimeout),
101            "http1_only" => Ok(Self::Http1Only),
102            "http2_only" => Ok(Self::Http2Only),
103            "http2_keep_alive_interval" => Ok(Self::Http2KeepAliveInterval),
104            "http2_keep_alive_timeout" => Ok(Self::Http2KeepAliveTimeout),
105            "http2_keep_alive_while_idle" => Ok(Self::Http2KeepAliveWhileIdle),
106            "http2_max_frame_size" => Ok(Self::Http2MaxFrameSize),
107            "pool_idle_timeout" => Ok(Self::PoolIdleTimeout),
108            "pool_max_idle_per_host" => Ok(Self::PoolMaxIdlePerHost),
109            "proxy_url" => Ok(Self::ProxyUrl),
110            "proxy_ca_certificate" => Ok(Self::ProxyCaCertificate),
111            "proxy_excludes" => Ok(Self::ProxyExcludes),
112            "timeout" => Ok(Self::Timeout),
113            "user_agent" => Ok(Self::UserAgent),
114            _ => Err(Error::UnknownConfigurationKey { key: s.into() }),
115        }
116    }
117}
118
119/// Represents a CA certificate provided by the user.
120///
121/// This is used to configure the client to trust a specific certificate. See
122/// [Self::from_pem] for an example
123#[derive(Debug, Clone)]
124pub struct Certificate(reqwest::tls::Certificate);
125
126impl Certificate {
127    /// Create a `Certificate` from a PEM encoded certificate.
128    ///
129    /// # Example from a PEM file
130    ///
131    /// ```no_run
132    /// # use olai_http::Certificate;
133    /// # use std::fs::File;
134    /// # use std::io::Read;
135    /// let mut buf = Vec::new();
136    /// File::open("my_cert.pem").unwrap()
137    ///   .read_to_end(&mut buf).unwrap();
138    /// let cert = Certificate::from_pem(&buf).unwrap();
139    ///
140    /// ```
141    pub fn from_pem(pem: &[u8]) -> Result<Self> {
142        Ok(Self(
143            reqwest::tls::Certificate::from_pem(pem).map_err(map_client_error)?,
144        ))
145    }
146
147    /// Create a collection of `Certificate` from a PEM encoded certificate
148    /// bundle.
149    ///
150    /// Files that contain such collections have extensions such as `.crt`,
151    /// `.cer` and `.pem` files.
152    pub fn from_pem_bundle(pem_bundle: &[u8]) -> Result<Vec<Self>> {
153        Ok(reqwest::tls::Certificate::from_pem_bundle(pem_bundle)
154            .map_err(map_client_error)?
155            .into_iter()
156            .map(Self)
157            .collect())
158    }
159
160    /// Create a `Certificate` from a binary DER encoded certificate.
161    pub fn from_der(der: &[u8]) -> Result<Self> {
162        Ok(Self(
163            reqwest::tls::Certificate::from_der(der).map_err(map_client_error)?,
164        ))
165    }
166}
167
168/// HTTP client configuration for remote object stores
169#[derive(Debug, Clone)]
170pub struct ClientOptions {
171    user_agent: Option<ConfigValue<HeaderValue>>,
172    root_certificates: Vec<Certificate>,
173    default_headers: Option<HeaderMap>,
174    proxy_url: Option<String>,
175    proxy_ca_certificate: Option<String>,
176    proxy_excludes: Option<String>,
177    allow_http: ConfigValue<bool>,
178    allow_insecure: ConfigValue<bool>,
179    timeout: Option<ConfigValue<Duration>>,
180    connect_timeout: Option<ConfigValue<Duration>>,
181    pool_idle_timeout: Option<ConfigValue<Duration>>,
182    pool_max_idle_per_host: Option<ConfigValue<usize>>,
183    http2_keep_alive_interval: Option<ConfigValue<Duration>>,
184    http2_keep_alive_timeout: Option<ConfigValue<Duration>>,
185    http2_keep_alive_while_idle: ConfigValue<bool>,
186    http2_max_frame_size: Option<ConfigValue<u32>>,
187    http1_only: ConfigValue<bool>,
188    http2_only: ConfigValue<bool>,
189}
190
191impl Default for ClientOptions {
192    fn default() -> Self {
193        // Defaults based on
194        // <https://docs.aws.amazon.com/sdkref/latest/guide/feature-smart-config-defaults.html>
195        // <https://docs.aws.amazon.com/whitepapers/latest/s3-optimizing-performance-best-practices/timeouts-and-retries-for-latency-sensitive-applications.html>
196        // Which recommend a connection timeout of 3.1s and a request timeout of 2s
197        //
198        // As object store requests may involve the transfer of non-trivial volumes of data
199        // we opt for a slightly higher default timeout of 30 seconds
200        Self {
201            user_agent: None,
202            root_certificates: Default::default(),
203            default_headers: None,
204            proxy_url: None,
205            proxy_ca_certificate: None,
206            proxy_excludes: None,
207            allow_http: Default::default(),
208            allow_insecure: Default::default(),
209            timeout: Some(Duration::from_secs(30).into()),
210            connect_timeout: Some(Duration::from_secs(5).into()),
211            pool_idle_timeout: None,
212            pool_max_idle_per_host: None,
213            http2_keep_alive_interval: None,
214            http2_keep_alive_timeout: None,
215            http2_keep_alive_while_idle: Default::default(),
216            http2_max_frame_size: None,
217            // HTTP2 is known to be significantly slower than HTTP1, so we default
218            // to HTTP1 for now.
219            // https://github.com/apache/arrow-rs/issues/5194
220            http1_only: true.into(),
221            http2_only: Default::default(),
222        }
223    }
224}
225
226impl ClientOptions {
227    /// Create a new [`ClientOptions`] with default values
228    pub fn new() -> Self {
229        Default::default()
230    }
231
232    /// Set an option by key
233    pub fn with_config(mut self, key: ClientConfigKey, value: impl Into<String>) -> Self {
234        match key {
235            ClientConfigKey::AllowHttp => self.allow_http.parse(value),
236            ClientConfigKey::AllowInvalidCertificates => self.allow_insecure.parse(value),
237            ClientConfigKey::ConnectTimeout => {
238                self.connect_timeout = Some(ConfigValue::Deferred(value.into()))
239            }
240            ClientConfigKey::Http1Only => self.http1_only.parse(value),
241            ClientConfigKey::Http2Only => self.http2_only.parse(value),
242            ClientConfigKey::Http2KeepAliveInterval => {
243                self.http2_keep_alive_interval = Some(ConfigValue::Deferred(value.into()))
244            }
245            ClientConfigKey::Http2KeepAliveTimeout => {
246                self.http2_keep_alive_timeout = Some(ConfigValue::Deferred(value.into()))
247            }
248            ClientConfigKey::Http2KeepAliveWhileIdle => {
249                self.http2_keep_alive_while_idle.parse(value)
250            }
251            ClientConfigKey::Http2MaxFrameSize => {
252                self.http2_max_frame_size = Some(ConfigValue::Deferred(value.into()))
253            }
254            ClientConfigKey::PoolIdleTimeout => {
255                self.pool_idle_timeout = Some(ConfigValue::Deferred(value.into()))
256            }
257            ClientConfigKey::PoolMaxIdlePerHost => {
258                self.pool_max_idle_per_host = Some(ConfigValue::Deferred(value.into()))
259            }
260            ClientConfigKey::ProxyUrl => self.proxy_url = Some(value.into()),
261            ClientConfigKey::ProxyCaCertificate => self.proxy_ca_certificate = Some(value.into()),
262            ClientConfigKey::ProxyExcludes => self.proxy_excludes = Some(value.into()),
263            ClientConfigKey::Timeout => self.timeout = Some(ConfigValue::Deferred(value.into())),
264            ClientConfigKey::UserAgent => {
265                self.user_agent = Some(ConfigValue::Deferred(value.into()))
266            }
267        }
268        self
269    }
270
271    /// Get an option by key
272    pub fn get_config_value(&self, key: &ClientConfigKey) -> Option<String> {
273        match key {
274            ClientConfigKey::AllowHttp => Some(self.allow_http.to_string()),
275            ClientConfigKey::AllowInvalidCertificates => Some(self.allow_insecure.to_string()),
276            ClientConfigKey::ConnectTimeout => self.connect_timeout.as_ref().map(fmt_duration),
277            ClientConfigKey::Http1Only => Some(self.http1_only.to_string()),
278            ClientConfigKey::Http2KeepAliveInterval => {
279                self.http2_keep_alive_interval.as_ref().map(fmt_duration)
280            }
281            ClientConfigKey::Http2KeepAliveTimeout => {
282                self.http2_keep_alive_timeout.as_ref().map(fmt_duration)
283            }
284            ClientConfigKey::Http2KeepAliveWhileIdle => {
285                Some(self.http2_keep_alive_while_idle.to_string())
286            }
287            ClientConfigKey::Http2MaxFrameSize => {
288                self.http2_max_frame_size.as_ref().map(|v| v.to_string())
289            }
290            ClientConfigKey::Http2Only => Some(self.http2_only.to_string()),
291            ClientConfigKey::PoolIdleTimeout => self.pool_idle_timeout.as_ref().map(fmt_duration),
292            ClientConfigKey::PoolMaxIdlePerHost => {
293                self.pool_max_idle_per_host.as_ref().map(|v| v.to_string())
294            }
295            ClientConfigKey::ProxyUrl => self.proxy_url.clone(),
296            ClientConfigKey::ProxyCaCertificate => self.proxy_ca_certificate.clone(),
297            ClientConfigKey::ProxyExcludes => self.proxy_excludes.clone(),
298            ClientConfigKey::Timeout => self.timeout.as_ref().map(fmt_duration),
299            ClientConfigKey::UserAgent => self
300                .user_agent
301                .as_ref()
302                .and_then(|v| v.get().ok())
303                .and_then(|v| v.to_str().ok().map(|s| s.to_string())),
304        }
305    }
306
307    /// Sets the User-Agent header to be used by this client
308    ///
309    /// Default is based on the version of this crate
310    pub fn with_user_agent(mut self, agent: HeaderValue) -> Self {
311        self.user_agent = Some(agent.into());
312        self
313    }
314
315    /// Add a custom root certificate.
316    ///
317    /// This can be used to connect to a server that has a self-signed
318    /// certificate for example.
319    pub fn with_root_certificate(mut self, certificate: Certificate) -> Self {
320        self.root_certificates.push(certificate);
321        self
322    }
323
324    /// Sets the default headers for every request
325    pub fn with_default_headers(mut self, headers: HeaderMap) -> Self {
326        self.default_headers = Some(headers);
327        self
328    }
329
330    /// Sets what protocol is allowed. If `allow_http` is :
331    /// * false (default):  Only HTTPS are allowed
332    /// * true:  HTTP and HTTPS are allowed
333    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
334        self.allow_http = allow_http.into();
335        self
336    }
337    /// Allows connections to invalid SSL certificates
338    /// * false (default):  Only valid HTTPS certificates are allowed
339    /// * true:  All HTTPS certificates are allowed
340    ///
341    /// # Warning
342    ///
343    /// You should think very carefully before using this method. If
344    /// invalid certificates are trusted, *any* certificate for *any* site
345    /// will be trusted for use. This includes expired certificates. This
346    /// introduces significant vulnerabilities, and should only be used
347    /// as a last resort or for testing
348    pub fn with_allow_invalid_certificates(mut self, allow_insecure: bool) -> Self {
349        self.allow_insecure = allow_insecure.into();
350        self
351    }
352
353    /// Only use http1 connections
354    ///
355    /// This is on by default, since http2 is known to be significantly slower than http1.
356    pub fn with_http1_only(mut self) -> Self {
357        self.http2_only = false.into();
358        self.http1_only = true.into();
359        self
360    }
361
362    /// Only use http2 connections
363    pub fn with_http2_only(mut self) -> Self {
364        self.http1_only = false.into();
365        self.http2_only = true.into();
366        self
367    }
368
369    /// Use http2 if supported, otherwise use http1.
370    pub fn with_allow_http2(mut self) -> Self {
371        self.http1_only = false.into();
372        self.http2_only = false.into();
373        self
374    }
375
376    /// Set a proxy URL to use for requests
377    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
378        self.proxy_url = Some(proxy_url.into());
379        self
380    }
381
382    /// Set a trusted proxy CA certificate
383    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
384        self.proxy_ca_certificate = Some(proxy_ca_certificate.into());
385        self
386    }
387
388    /// Set a list of hosts to exclude from proxy connections
389    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
390        self.proxy_excludes = Some(proxy_excludes.into());
391        self
392    }
393
394    /// Set a request timeout
395    ///
396    /// The timeout is applied from when the request starts connecting until the
397    /// response body has finished
398    ///
399    /// Default is 30 seconds
400    pub fn with_timeout(mut self, timeout: Duration) -> Self {
401        self.timeout = Some(ConfigValue::Parsed(timeout));
402        self
403    }
404
405    /// Disables the request timeout
406    ///
407    /// See [`Self::with_timeout`]
408    pub fn with_timeout_disabled(mut self) -> Self {
409        self.timeout = None;
410        self
411    }
412
413    /// Set a timeout for only the connect phase of a Client
414    ///
415    /// Default is 5 seconds
416    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
417        self.connect_timeout = Some(ConfigValue::Parsed(timeout));
418        self
419    }
420
421    /// Disables the connection timeout
422    ///
423    /// See [`Self::with_connect_timeout`]
424    pub fn with_connect_timeout_disabled(mut self) -> Self {
425        self.connect_timeout = None;
426        self
427    }
428
429    /// Set the pool max idle timeout
430    ///
431    /// This is the length of time an idle connection will be kept alive
432    ///
433    /// Default is 90 seconds enforced by reqwest
434    pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
435        self.pool_idle_timeout = Some(ConfigValue::Parsed(timeout));
436        self
437    }
438
439    /// Set the maximum number of idle connections per host
440    ///
441    /// Default is no limit enforced by reqwest
442    pub fn with_pool_max_idle_per_host(mut self, max: usize) -> Self {
443        self.pool_max_idle_per_host = Some(max.into());
444        self
445    }
446
447    /// Sets an interval for HTTP2 Ping frames should be sent to keep a connection alive.
448    ///
449    /// Default is disabled enforced by reqwest
450    pub fn with_http2_keep_alive_interval(mut self, interval: Duration) -> Self {
451        self.http2_keep_alive_interval = Some(ConfigValue::Parsed(interval));
452        self
453    }
454
455    /// Sets a timeout for receiving an acknowledgement of the keep-alive ping.
456    ///
457    /// If the ping is not acknowledged within the timeout, the connection will be closed.
458    /// Does nothing if http2_keep_alive_interval is disabled.
459    ///
460    /// Default is disabled enforced by reqwest
461    pub fn with_http2_keep_alive_timeout(mut self, interval: Duration) -> Self {
462        self.http2_keep_alive_timeout = Some(ConfigValue::Parsed(interval));
463        self
464    }
465
466    /// Enable HTTP2 keep alive pings for idle connections
467    ///
468    /// If disabled, keep-alive pings are only sent while there are open request/response
469    /// streams. If enabled, pings are also sent when no streams are active
470    ///
471    /// Default is disabled enforced by reqwest
472    pub fn with_http2_keep_alive_while_idle(mut self) -> Self {
473        self.http2_keep_alive_while_idle = true.into();
474        self
475    }
476
477    /// Sets the maximum frame size to use for HTTP2.
478    ///
479    /// Default is currently 16,384 but may change internally to optimize for common uses.
480    pub fn with_http2_max_frame_size(mut self, sz: u32) -> Self {
481        self.http2_max_frame_size = Some(ConfigValue::Parsed(sz));
482        self
483    }
484
485    /// Create a [`Client`] with overrides optimised for metadata endpoint access
486    ///
487    /// In particular:
488    /// * Allows HTTP as metadata endpoints do not use TLS
489    /// * Configures a low connection timeout to provide quick feedback if not present
490    pub(crate) fn metadata_client(&self) -> Result<Client> {
491        self.clone()
492            .with_allow_http(true)
493            .with_connect_timeout(Duration::from_secs(1))
494            .client()
495    }
496
497    pub(crate) fn client(&self) -> Result<Client> {
498        let mut builder = ClientBuilder::new();
499
500        match &self.user_agent {
501            Some(user_agent) => builder = builder.user_agent(user_agent.get()?),
502            None => builder = builder.user_agent(DEFAULT_USER_AGENT),
503        }
504
505        if let Some(headers) = &self.default_headers {
506            builder = builder.default_headers(headers.clone())
507        }
508
509        if let Some(proxy) = &self.proxy_url {
510            let mut proxy = Proxy::all(proxy).map_err(map_client_error)?;
511
512            if let Some(certificate) = &self.proxy_ca_certificate {
513                let certificate = reqwest::tls::Certificate::from_pem(certificate.as_bytes())
514                    .map_err(map_client_error)?;
515
516                builder = builder.add_root_certificate(certificate);
517            }
518
519            if let Some(proxy_excludes) = &self.proxy_excludes {
520                let no_proxy = NoProxy::from_string(proxy_excludes);
521
522                proxy = proxy.no_proxy(no_proxy);
523            }
524
525            builder = builder.proxy(proxy);
526        }
527
528        for certificate in &self.root_certificates {
529            builder = builder.add_root_certificate(certificate.0.clone());
530        }
531
532        if let Some(timeout) = &self.timeout {
533            builder = builder.timeout(timeout.get()?)
534        }
535
536        if let Some(timeout) = &self.connect_timeout {
537            builder = builder.connect_timeout(timeout.get()?)
538        }
539
540        if let Some(timeout) = &self.pool_idle_timeout {
541            builder = builder.pool_idle_timeout(timeout.get()?)
542        }
543
544        if let Some(max) = &self.pool_max_idle_per_host {
545            builder = builder.pool_max_idle_per_host(max.get()?)
546        }
547
548        if let Some(interval) = &self.http2_keep_alive_interval {
549            builder = builder.http2_keep_alive_interval(interval.get()?)
550        }
551
552        if let Some(interval) = &self.http2_keep_alive_timeout {
553            builder = builder.http2_keep_alive_timeout(interval.get()?)
554        }
555
556        if self.http2_keep_alive_while_idle.get()? {
557            builder = builder.http2_keep_alive_while_idle(true)
558        }
559
560        if let Some(sz) = &self.http2_max_frame_size {
561            builder = builder.http2_max_frame_size(Some(sz.get()?))
562        }
563
564        if self.http1_only.get()? {
565            builder = builder.http1_only()
566        }
567
568        if self.http2_only.get()? {
569            builder = builder.http2_prior_knowledge()
570        }
571
572        if self.allow_insecure.get()? {
573            builder = builder.danger_accept_invalid_certs(true)
574        }
575
576        // Reqwest will remove the `Content-Length` header if it is configured to
577        // transparently decompress the body via the non-default `gzip` feature.
578        builder = builder.no_gzip();
579
580        builder
581            .https_only(!self.allow_http.get()?)
582            .build()
583            .map_err(map_client_error)
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use std::collections::HashMap;
591
592    #[test]
593    fn client_test_config_from_map() {
594        let allow_http = "true".to_string();
595        let allow_invalid_certificates = "false".to_string();
596        let connect_timeout = "90 seconds".to_string();
597        let http1_only = "true".to_string();
598        let http2_only = "false".to_string();
599        let http2_keep_alive_interval = "90 seconds".to_string();
600        let http2_keep_alive_timeout = "91 seconds".to_string();
601        let http2_keep_alive_while_idle = "92 seconds".to_string();
602        let http2_max_frame_size = "1337".to_string();
603        let pool_idle_timeout = "93 seconds".to_string();
604        let pool_max_idle_per_host = "94".to_string();
605        let proxy_url = "https://fake_proxy_url".to_string();
606        let timeout = "95 seconds".to_string();
607        let user_agent = "object_store:fake_user_agent".to_string();
608
609        let options = HashMap::from([
610            ("allow_http", allow_http.clone()),
611            (
612                "allow_invalid_certificates",
613                allow_invalid_certificates.clone(),
614            ),
615            ("connect_timeout", connect_timeout.clone()),
616            ("http1_only", http1_only.clone()),
617            ("http2_only", http2_only.clone()),
618            (
619                "http2_keep_alive_interval",
620                http2_keep_alive_interval.clone(),
621            ),
622            ("http2_keep_alive_timeout", http2_keep_alive_timeout.clone()),
623            (
624                "http2_keep_alive_while_idle",
625                http2_keep_alive_while_idle.clone(),
626            ),
627            ("http2_max_frame_size", http2_max_frame_size.clone()),
628            ("pool_idle_timeout", pool_idle_timeout.clone()),
629            ("pool_max_idle_per_host", pool_max_idle_per_host.clone()),
630            ("proxy_url", proxy_url.clone()),
631            ("timeout", timeout.clone()),
632            ("user_agent", user_agent.clone()),
633        ]);
634
635        let builder = options
636            .into_iter()
637            .fold(ClientOptions::new(), |builder, (key, value)| {
638                builder.with_config(key.parse().unwrap(), value)
639            });
640
641        assert_eq!(
642            builder
643                .get_config_value(&ClientConfigKey::AllowHttp)
644                .unwrap(),
645            allow_http
646        );
647        assert_eq!(
648            builder
649                .get_config_value(&ClientConfigKey::AllowInvalidCertificates)
650                .unwrap(),
651            allow_invalid_certificates
652        );
653        assert_eq!(
654            builder
655                .get_config_value(&ClientConfigKey::ConnectTimeout)
656                .unwrap(),
657            connect_timeout
658        );
659        assert_eq!(
660            builder
661                .get_config_value(&ClientConfigKey::Http1Only)
662                .unwrap(),
663            http1_only
664        );
665        assert_eq!(
666            builder
667                .get_config_value(&ClientConfigKey::Http2Only)
668                .unwrap(),
669            http2_only
670        );
671        assert_eq!(
672            builder
673                .get_config_value(&ClientConfigKey::Http2KeepAliveInterval)
674                .unwrap(),
675            http2_keep_alive_interval
676        );
677        assert_eq!(
678            builder
679                .get_config_value(&ClientConfigKey::Http2KeepAliveTimeout)
680                .unwrap(),
681            http2_keep_alive_timeout
682        );
683        assert_eq!(
684            builder
685                .get_config_value(&ClientConfigKey::Http2KeepAliveWhileIdle)
686                .unwrap(),
687            http2_keep_alive_while_idle
688        );
689        assert_eq!(
690            builder
691                .get_config_value(&ClientConfigKey::Http2MaxFrameSize)
692                .unwrap(),
693            http2_max_frame_size
694        );
695
696        assert_eq!(
697            builder
698                .get_config_value(&ClientConfigKey::PoolIdleTimeout)
699                .unwrap(),
700            pool_idle_timeout
701        );
702        assert_eq!(
703            builder
704                .get_config_value(&ClientConfigKey::PoolMaxIdlePerHost)
705                .unwrap(),
706            pool_max_idle_per_host
707        );
708        assert_eq!(
709            builder
710                .get_config_value(&ClientConfigKey::ProxyUrl)
711                .unwrap(),
712            proxy_url
713        );
714        assert_eq!(
715            builder.get_config_value(&ClientConfigKey::Timeout).unwrap(),
716            timeout
717        );
718        assert_eq!(
719            builder
720                .get_config_value(&ClientConfigKey::UserAgent)
721                .unwrap(),
722            user_agent
723        );
724    }
725}