Skip to main content

object_store/client/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Generic utilities for network based [`ObjectStore`] implementations
19//!
20//! [`ObjectStore`]: crate::ObjectStore
21
22pub(crate) mod backoff;
23
24#[cfg(not(target_arch = "wasm32"))]
25mod dns;
26#[cfg(not(target_arch = "wasm32"))]
27pub use dns::{DnsError, DnsFuture, DnsResolver};
28
29#[cfg(not(target_arch = "wasm32"))]
30#[cfg(test)]
31pub(crate) mod mock_server;
32
33pub(crate) mod retry;
34
35#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
36pub(crate) mod pagination;
37
38pub(crate) mod get;
39
40#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
41pub(crate) mod list;
42
43#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
44pub(crate) mod token;
45
46pub(crate) mod header;
47
48#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
49pub(crate) mod s3;
50
51pub(crate) mod builder;
52mod http;
53
54#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
55pub(crate) mod parts;
56pub use http::*;
57
58#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
59mod crypto;
60
61#[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
62pub use crypto::*;
63
64use ::http::header::{HeaderMap, HeaderValue};
65use async_trait::async_trait;
66use serde::{Deserialize, Serialize};
67use std::collections::HashMap;
68use std::str::FromStr;
69use std::sync::Arc;
70use std::time::Duration;
71
72#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
73use reqwest::{NoProxy, Proxy};
74
75use crate::config::{ConfigValue, fmt_duration};
76use crate::path::Path;
77use crate::{GetOptions, Result};
78
79#[cfg(feature = "reqwest")]
80fn map_client_error(e: reqwest::Error) -> super::Error {
81    super::Error::Generic {
82        store: "HTTP client",
83        source: Box::new(e),
84    }
85}
86
87#[cfg(feature = "reqwest")]
88static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
89
90/// Configuration keys for [`ClientOptions`]
91#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Deserialize, Serialize)]
92#[non_exhaustive]
93pub enum ClientConfigKey {
94    /// Allow non-TLS, i.e. non-HTTPS connections
95    ///
96    /// Supported keys:
97    /// - `allow_http`
98    AllowHttp,
99    /// Skip certificate validation on https connections.
100    ///
101    /// <div class="warning">
102    ///
103    /// **Warning**
104    ///
105    /// You should think very carefully before using this method. If
106    /// invalid certificates are trusted, *any* certificate for *any* site
107    /// will be trusted for use. This includes expired certificates. This
108    /// introduces significant vulnerabilities, and should only be used
109    /// as a last resort or for testing
110    ///
111    /// </div>
112    ///
113    /// Supported keys:
114    /// - `allow_invalid_certificates`
115    AllowInvalidCertificates,
116    /// Disable certificate validation using the operating system's certificate facilities.
117    ///
118    /// See [`ClientOptions::with_no_system_certificates`]
119    ///
120    /// Supported keys:
121    ///
122    /// - `disable_system_certificates`
123    NoSystemCertificates,
124    /// Timeout for only the connect phase of a Client
125    ///
126    /// Supported keys:
127    /// - `connect_timeout`
128    ConnectTimeout,
129    /// default [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type) for uploads
130    ///
131    /// Supported keys:
132    /// - `default_content_type`
133    DefaultContentType,
134    /// Only use HTTP/1 connections
135    ///
136    /// Supported keys:
137    /// - `http1_only`
138    Http1Only,
139    /// Interval for HTTP/2 Ping frames should be sent to keep a connection alive.
140    ///
141    /// Supported keys:
142    /// - `http2_keep_alive_interval`
143    Http2KeepAliveInterval,
144    /// Timeout for receiving an acknowledgement of the keep-alive ping.
145    ///
146    /// Supported keys:
147    /// - `http2_keep_alive_timeout`
148    Http2KeepAliveTimeout,
149    /// Enable HTTP/2 keep alive pings for idle connections
150    ///
151    /// Supported keys:
152    /// - `http2_keep_alive_while_idle`
153    Http2KeepAliveWhileIdle,
154    /// Sets the maximum frame size to use for HTTP/2.
155    ///
156    /// Supported keys:
157    /// - `http2_max_frame_size`
158    Http2MaxFrameSize,
159    /// Only use HTTP/2 connections
160    ///
161    /// Supported keys:
162    /// - `http2_only`
163    Http2Only,
164    /// The pool max idle timeout
165    ///
166    /// This is the length of time an idle connection will be kept alive
167    ///
168    /// Supported keys:
169    /// - `pool_idle_timeout`
170    PoolIdleTimeout,
171    /// maximum number of idle connections per host
172    ///
173    /// Supported keys:
174    /// - `pool_max_idle_per_host`
175    PoolMaxIdlePerHost,
176    /// HTTP proxy to use for requests
177    ///
178    /// Supported keys:
179    /// - `proxy_url`
180    ProxyUrl,
181    /// PEM-formatted CA certificate for proxy connections
182    ///
183    /// Supported keys:
184    /// - `proxy_ca_certificate`
185    ProxyCaCertificate,
186    /// List of hosts that bypass proxy
187    ///
188    /// Supported keys:
189    /// - `proxy_excludes`
190    ProxyExcludes,
191    /// Randomize order addresses that the DNS resolution yields.
192    ///
193    /// This will spread the connections across more servers.
194    ///
195    /// <div class="warning">
196    ///
197    /// **Warning**
198    ///
199    /// This will override the DNS resolver configured by [`reqwest`].
200    ///
201    /// </div>
202    ///
203    /// Supported keys:
204    /// - `randomize_addresses`
205    RandomizeAddresses,
206    /// Read timeout
207    ///
208    /// The timeout applies to each read operation, and resets after a
209    /// successful read. This is useful for detecting stalled connections
210    /// when the size of the response is not known beforehand.
211    ///
212    /// Supported keys:
213    /// - `read_timeout`
214    ReadTimeout,
215    /// Request timeout
216    ///
217    /// The timeout is applied from when the request starts connecting until the
218    /// response body has finished
219    ///
220    /// Supported keys:
221    /// - `timeout`
222    Timeout,
223    /// User-Agent header to be used by this client
224    ///
225    /// Supported keys:
226    /// - `user_agent`
227    UserAgent,
228}
229
230impl AsRef<str> for ClientConfigKey {
231    fn as_ref(&self) -> &str {
232        match self {
233            Self::AllowHttp => "allow_http",
234            Self::AllowInvalidCertificates => "allow_invalid_certificates",
235            Self::NoSystemCertificates => "disable_system_certificates",
236            Self::ConnectTimeout => "connect_timeout",
237            Self::DefaultContentType => "default_content_type",
238            Self::Http1Only => "http1_only",
239            Self::Http2Only => "http2_only",
240            Self::Http2KeepAliveInterval => "http2_keep_alive_interval",
241            Self::Http2KeepAliveTimeout => "http2_keep_alive_timeout",
242            Self::Http2KeepAliveWhileIdle => "http2_keep_alive_while_idle",
243            Self::Http2MaxFrameSize => "http2_max_frame_size",
244            Self::PoolIdleTimeout => "pool_idle_timeout",
245            Self::PoolMaxIdlePerHost => "pool_max_idle_per_host",
246            Self::ProxyUrl => "proxy_url",
247            Self::ProxyCaCertificate => "proxy_ca_certificate",
248            Self::ProxyExcludes => "proxy_excludes",
249            Self::RandomizeAddresses => "randomize_addresses",
250            Self::ReadTimeout => "read_timeout",
251            Self::Timeout => "timeout",
252            Self::UserAgent => "user_agent",
253        }
254    }
255}
256
257impl FromStr for ClientConfigKey {
258    type Err = super::Error;
259
260    fn from_str(s: &str) -> Result<Self, Self::Err> {
261        match s {
262            "allow_http" => Ok(Self::AllowHttp),
263            "allow_invalid_certificates" => Ok(Self::AllowInvalidCertificates),
264            "disable_system_certificates" => Ok(Self::NoSystemCertificates),
265            "connect_timeout" => Ok(Self::ConnectTimeout),
266            "default_content_type" => Ok(Self::DefaultContentType),
267            "http1_only" => Ok(Self::Http1Only),
268            "http2_only" => Ok(Self::Http2Only),
269            "http2_keep_alive_interval" => Ok(Self::Http2KeepAliveInterval),
270            "http2_keep_alive_timeout" => Ok(Self::Http2KeepAliveTimeout),
271            "http2_keep_alive_while_idle" => Ok(Self::Http2KeepAliveWhileIdle),
272            "http2_max_frame_size" => Ok(Self::Http2MaxFrameSize),
273            "pool_idle_timeout" => Ok(Self::PoolIdleTimeout),
274            "pool_max_idle_per_host" => Ok(Self::PoolMaxIdlePerHost),
275            "proxy_url" => Ok(Self::ProxyUrl),
276            "proxy_ca_certificate" => Ok(Self::ProxyCaCertificate),
277            "proxy_excludes" => Ok(Self::ProxyExcludes),
278            "randomize_addresses" => Ok(Self::RandomizeAddresses),
279            "read_timeout" => Ok(Self::ReadTimeout),
280            "timeout" => Ok(Self::Timeout),
281            "user_agent" => Ok(Self::UserAgent),
282            _ => Err(super::Error::UnknownConfigurationKey {
283                store: "HTTP",
284                key: s.into(),
285            }),
286        }
287    }
288}
289
290/// Represents a CA certificate provided by the user.
291///
292/// This is used to configure the client to trust a specific certificate. See
293/// [Self::from_pem] for an example
294#[derive(Debug, Clone)]
295#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
296pub struct Certificate(reqwest::tls::Certificate);
297
298#[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
299impl Certificate {
300    /// Create a `Certificate` from a PEM encoded certificate.
301    ///
302    /// # Example from a PEM file
303    ///
304    /// ```no_run
305    /// # use object_store::Certificate;
306    /// # use std::fs::File;
307    /// # use std::io::Read;
308    /// let mut buf = Vec::new();
309    /// File::open("my_cert.pem").unwrap()
310    ///   .read_to_end(&mut buf).unwrap();
311    /// let cert = Certificate::from_pem(&buf).unwrap();
312    ///
313    /// ```
314    pub fn from_pem(pem: &[u8]) -> Result<Self> {
315        Ok(Self(
316            reqwest::tls::Certificate::from_pem(pem).map_err(map_client_error)?,
317        ))
318    }
319
320    /// Create a collection of `Certificate` from a PEM encoded certificate
321    /// bundle.
322    ///
323    /// Files that contain such collections have extensions such as `.crt`,
324    /// `.cer` and `.pem` files.
325    pub fn from_pem_bundle(pem_bundle: &[u8]) -> Result<Vec<Self>> {
326        Ok(reqwest::tls::Certificate::from_pem_bundle(pem_bundle)
327            .map_err(map_client_error)?
328            .into_iter()
329            .map(Self)
330            .collect())
331    }
332
333    /// Create a `Certificate` from a binary DER encoded certificate.
334    pub fn from_der(der: &[u8]) -> Result<Self> {
335        Ok(Self(
336            reqwest::tls::Certificate::from_der(der).map_err(map_client_error)?,
337        ))
338    }
339}
340
341/// HTTP client configuration for remote object stores
342#[derive(Debug, Clone)]
343pub struct ClientOptions {
344    user_agent: Option<ConfigValue<HeaderValue>>,
345    #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
346    root_certificates: Vec<Certificate>,
347    no_system_certificates: ConfigValue<bool>,
348    content_type_map: HashMap<String, String>,
349    default_content_type: Option<String>,
350    default_headers: Option<HeaderMap>,
351    proxy_url: Option<String>,
352    proxy_ca_certificate: Option<String>,
353    proxy_excludes: Option<String>,
354    allow_http: ConfigValue<bool>,
355    allow_invalid_certificates: ConfigValue<bool>,
356    timeout: Option<ConfigValue<Duration>>,
357    connect_timeout: Option<ConfigValue<Duration>>,
358    read_timeout: Option<ConfigValue<Duration>>,
359    pool_idle_timeout: Option<ConfigValue<Duration>>,
360    pool_max_idle_per_host: Option<ConfigValue<usize>>,
361    http2_keep_alive_interval: Option<ConfigValue<Duration>>,
362    http2_keep_alive_timeout: Option<ConfigValue<Duration>>,
363    http2_keep_alive_while_idle: ConfigValue<bool>,
364    http2_max_frame_size: Option<ConfigValue<u32>>,
365    http1_only: ConfigValue<bool>,
366    http2_only: ConfigValue<bool>,
367    randomize_addresses: ConfigValue<bool>,
368    #[cfg(not(target_arch = "wasm32"))]
369    dns_resolver: Option<Arc<dyn DnsResolver>>,
370}
371
372impl Default for ClientOptions {
373    fn default() -> Self {
374        // Defaults based on
375        // <https://docs.aws.amazon.com/sdkref/latest/guide/feature-smart-config-defaults.html>
376        // <https://docs.aws.amazon.com/whitepapers/latest/s3-optimizing-performance-best-practices/timeouts-and-retries-for-latency-sensitive-applications.html>
377        // Which recommend a connection timeout of 3.1s and a request timeout of 2s
378        //
379        // As object store requests may involve the transfer of non-trivial volumes of data
380        // we opt for a slightly higher default timeout of 30 seconds
381        Self {
382            user_agent: None,
383            #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
384            root_certificates: Default::default(),
385            no_system_certificates: false.into(),
386            content_type_map: Default::default(),
387            default_content_type: None,
388            default_headers: None,
389            proxy_url: None,
390            proxy_ca_certificate: None,
391            proxy_excludes: None,
392            allow_http: Default::default(),
393            allow_invalid_certificates: Default::default(),
394            timeout: Some(Duration::from_secs(30).into()),
395            connect_timeout: Some(Duration::from_secs(5).into()),
396            read_timeout: None,
397            pool_idle_timeout: None,
398            pool_max_idle_per_host: None,
399            http2_keep_alive_interval: None,
400            http2_keep_alive_timeout: None,
401            http2_keep_alive_while_idle: Default::default(),
402            http2_max_frame_size: None,
403            // HTTP/2 is known to be significantly slower than HTTP/1, so we default
404            // to HTTP/1 for now.
405            // https://github.com/apache/arrow-rs/issues/5194
406            http1_only: true.into(),
407            http2_only: Default::default(),
408            randomize_addresses: true.into(),
409            #[cfg(not(target_arch = "wasm32"))]
410            dns_resolver: Default::default(),
411        }
412    }
413}
414
415impl ClientOptions {
416    /// Create a new [`ClientOptions`] with default values
417    pub fn new() -> Self {
418        Default::default()
419    }
420
421    /// Set an option by key
422    pub fn with_config(mut self, key: ClientConfigKey, value: impl Into<String>) -> Self {
423        match key {
424            ClientConfigKey::AllowHttp => self.allow_http.parse(value),
425            ClientConfigKey::AllowInvalidCertificates => {
426                self.allow_invalid_certificates.parse(value)
427            }
428            ClientConfigKey::NoSystemCertificates => self.no_system_certificates.parse(value),
429            ClientConfigKey::ConnectTimeout => {
430                self.connect_timeout = Some(ConfigValue::Deferred(value.into()))
431            }
432            ClientConfigKey::ReadTimeout => {
433                self.read_timeout = Some(ConfigValue::Deferred(value.into()))
434            }
435            ClientConfigKey::DefaultContentType => self.default_content_type = Some(value.into()),
436            ClientConfigKey::Http1Only => self.http1_only.parse(value),
437            ClientConfigKey::Http2Only => self.http2_only.parse(value),
438            ClientConfigKey::Http2KeepAliveInterval => {
439                self.http2_keep_alive_interval = Some(ConfigValue::Deferred(value.into()))
440            }
441            ClientConfigKey::Http2KeepAliveTimeout => {
442                self.http2_keep_alive_timeout = Some(ConfigValue::Deferred(value.into()))
443            }
444            ClientConfigKey::Http2KeepAliveWhileIdle => {
445                self.http2_keep_alive_while_idle.parse(value)
446            }
447            ClientConfigKey::Http2MaxFrameSize => {
448                self.http2_max_frame_size = Some(ConfigValue::Deferred(value.into()))
449            }
450            ClientConfigKey::PoolIdleTimeout => {
451                self.pool_idle_timeout = Some(ConfigValue::Deferred(value.into()))
452            }
453            ClientConfigKey::PoolMaxIdlePerHost => {
454                self.pool_max_idle_per_host = Some(ConfigValue::Deferred(value.into()))
455            }
456            ClientConfigKey::ProxyUrl => self.proxy_url = Some(value.into()),
457            ClientConfigKey::ProxyCaCertificate => self.proxy_ca_certificate = Some(value.into()),
458            ClientConfigKey::ProxyExcludes => self.proxy_excludes = Some(value.into()),
459            ClientConfigKey::RandomizeAddresses => {
460                self.randomize_addresses.parse(value);
461            }
462            ClientConfigKey::Timeout => self.timeout = Some(ConfigValue::Deferred(value.into())),
463            ClientConfigKey::UserAgent => {
464                self.user_agent = Some(ConfigValue::Deferred(value.into()))
465            }
466        }
467        self
468    }
469
470    /// Get an option by key
471    pub fn get_config_value(&self, key: &ClientConfigKey) -> Option<String> {
472        match key {
473            ClientConfigKey::AllowHttp => Some(self.allow_http.to_string()),
474            ClientConfigKey::AllowInvalidCertificates => {
475                Some(self.allow_invalid_certificates.to_string())
476            }
477            ClientConfigKey::NoSystemCertificates => Some(self.no_system_certificates.to_string()),
478            ClientConfigKey::ConnectTimeout => self.connect_timeout.as_ref().map(fmt_duration),
479            ClientConfigKey::ReadTimeout => self.read_timeout.as_ref().map(fmt_duration),
480            ClientConfigKey::DefaultContentType => self.default_content_type.clone(),
481            ClientConfigKey::Http1Only => Some(self.http1_only.to_string()),
482            ClientConfigKey::Http2KeepAliveInterval => {
483                self.http2_keep_alive_interval.as_ref().map(fmt_duration)
484            }
485            ClientConfigKey::Http2KeepAliveTimeout => {
486                self.http2_keep_alive_timeout.as_ref().map(fmt_duration)
487            }
488            ClientConfigKey::Http2KeepAliveWhileIdle => {
489                Some(self.http2_keep_alive_while_idle.to_string())
490            }
491            ClientConfigKey::Http2MaxFrameSize => {
492                self.http2_max_frame_size.as_ref().map(|v| v.to_string())
493            }
494            ClientConfigKey::Http2Only => Some(self.http2_only.to_string()),
495            ClientConfigKey::PoolIdleTimeout => self.pool_idle_timeout.as_ref().map(fmt_duration),
496            ClientConfigKey::PoolMaxIdlePerHost => {
497                self.pool_max_idle_per_host.as_ref().map(|v| v.to_string())
498            }
499            ClientConfigKey::ProxyUrl => self.proxy_url.clone(),
500            ClientConfigKey::ProxyCaCertificate => self.proxy_ca_certificate.clone(),
501            ClientConfigKey::ProxyExcludes => self.proxy_excludes.clone(),
502            ClientConfigKey::RandomizeAddresses => Some(self.randomize_addresses.to_string()),
503            ClientConfigKey::Timeout => self.timeout.as_ref().map(fmt_duration),
504            ClientConfigKey::UserAgent => self
505                .user_agent
506                .as_ref()
507                .and_then(|v| v.get().ok())
508                .and_then(|v| v.to_str().ok().map(|s| s.to_string())),
509        }
510    }
511
512    /// Sets the [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) header to be used by this client
513    ///
514    /// Default is based on the version of this crate
515    pub fn with_user_agent(mut self, agent: HeaderValue) -> Self {
516        self.user_agent = Some(agent.into());
517        self
518    }
519
520    /// Add a custom root certificate.
521    ///
522    /// This can be used to connect to a server that has a self-signed
523    /// certificate for example.
524    #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
525    pub fn with_root_certificate(mut self, certificate: Certificate) -> Self {
526        self.root_certificates.push(certificate);
527        self
528    }
529
530    /// Set the default [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type) for uploads
531    pub fn with_default_content_type(mut self, mime: impl Into<String>) -> Self {
532        self.default_content_type = Some(mime.into());
533        self
534    }
535
536    /// Set the [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type) for a given file extension
537    pub fn with_content_type_for_suffix(
538        mut self,
539        extension: impl Into<String>,
540        mime: impl Into<String>,
541    ) -> Self {
542        self.content_type_map.insert(extension.into(), mime.into());
543        self
544    }
545
546    /// Sets the default headers for every request
547    pub fn with_default_headers(mut self, headers: HeaderMap) -> Self {
548        self.default_headers = Some(headers);
549        self
550    }
551
552    /// Sets what protocol is allowed.
553    ///
554    /// If `allow_http` is :
555    /// * `false` (default):  Only HTTPS is allowed
556    /// * `true`:  HTTP and HTTPS are allowed
557    pub fn with_allow_http(mut self, allow_http: bool) -> Self {
558        self.allow_http = allow_http.into();
559        self
560    }
561
562    /// Allows connections to invalid SSL certificates
563    ///
564    /// If `allow_invalid_certificates` is :
565    /// * `false` (default):  Only valid HTTPS certificates are allowed
566    /// * `true`:  All HTTPS certificates are allowed
567    ///
568    /// <div class="warning">
569    ///
570    /// **Warning**
571    ///
572    /// You should think very carefully before using this method. If
573    /// invalid certificates are trusted, *any* certificate for *any* site
574    /// will be trusted for use. This includes expired certificates. This
575    /// introduces significant vulnerabilities, and should only be used
576    /// as a last resort or for testing
577    ///
578    /// </div>
579    pub fn with_allow_invalid_certificates(mut self, allow_invalid_certificates: bool) -> Self {
580        self.allow_invalid_certificates = allow_invalid_certificates.into();
581        self
582    }
583
584    /// Disable certificates provided by the system
585    ///
586    /// By default TLS certificates are validated using [`rustls-platform-verifier`],
587    /// which makes use of the system's trust store, in addition to any certificates
588    /// registered using [`Self::with_root_certificate`]. If disabled, instead [`rustls-webpki`]
589    /// is used with only the certificates registered using [`Self::with_root_certificate`].
590    ///
591    /// [`rustls-platform-verifier`]: https://crates.io/crates/rustls-platform-verifier
592    /// [`rustls-webpki`]: https://crates.io/crates/rustls-webpki
593    pub fn with_no_system_certificates(mut self, no_certs: bool) -> Self {
594        self.no_system_certificates = no_certs.into();
595        self
596    }
597
598    /// Only use HTTP/1 connections (default)
599    ///
600    /// # See Also
601    /// * [`Self::with_http2_only`] if you only want to use HTTP/2
602    /// * [`Self::with_allow_http2`] if you want to use HTTP/1 or HTTP/2
603    ///
604    /// <div class="warning">
605    /// HTTP/2 is not used by default. See details [#104](https://github.com/apache/arrow-rs-object-store/issues/104)
606    /// </div>
607    pub fn with_http1_only(mut self) -> Self {
608        self.http2_only = false.into();
609        self.http1_only = true.into();
610        self
611    }
612
613    /// Only use HTTP/2 connections
614    ///
615    /// # See Also
616    /// * [`Self::with_http1_only`] if you only want to use HTTP/1
617    /// * [`Self::with_allow_http2`] if you want to use HTTP/1 or HTTP/2
618    ///
619    /// <div class="warning">
620    /// HTTP/2 is not used by default. See details [#104](https://github.com/apache/arrow-rs-object-store/issues/104)
621    /// </div>
622    pub fn with_http2_only(mut self) -> Self {
623        self.http1_only = false.into();
624        self.http2_only = true.into();
625        self
626    }
627
628    /// Use HTTP/2 if supported, otherwise use HTTP/1.
629    ///
630    /// # See Also
631    /// * [`Self::with_http1_only`] if you only want to use HTTP/1
632    /// * [`Self::with_http2_only`] if you only want to use HTTP/2
633    ///
634    /// <div class="warning">
635    /// HTTP/2 is not used by default. See details [#104](https://github.com/apache/arrow-rs-object-store/issues/104)
636    /// </div>
637    pub fn with_allow_http2(mut self) -> Self {
638        self.http1_only = false.into();
639        self.http2_only = false.into();
640        self
641    }
642
643    /// Set a proxy URL to use for requests
644    pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
645        self.proxy_url = Some(proxy_url.into());
646        self
647    }
648
649    /// Set a trusted proxy CA certificate
650    pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
651        self.proxy_ca_certificate = Some(proxy_ca_certificate.into());
652        self
653    }
654
655    /// Set a list of hosts to exclude from proxy connections
656    pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
657        self.proxy_excludes = Some(proxy_excludes.into());
658        self
659    }
660
661    /// Set timeout for the overall request
662    ///
663    /// The timeout starts from when the request starts connecting until the
664    /// response body has finished. If the request does not complete within the
665    /// timeout, the client returns a timeout error.
666    ///
667    /// Timeout errors are retried, subject to the [`RetryConfig`]
668    ///
669    /// Default is 30 seconds
670    ///
671    /// # See Also
672    /// * [`Self::with_timeout_disabled`] to disable the timeout
673    /// * [`Self::with_connect_timeout`] to set a timeout for the connect phase
674    ///
675    /// [`RetryConfig`]: crate::RetryConfig
676    pub fn with_timeout(mut self, timeout: Duration) -> Self {
677        self.timeout = Some(ConfigValue::Parsed(timeout));
678        self
679    }
680
681    /// Disables the request timeout
682    ///
683    /// # See Also
684    /// * [`Self::with_timeout`]
685    pub fn with_timeout_disabled(mut self) -> Self {
686        self.timeout = None;
687        self
688    }
689
690    /// Set a timeout for only the connect phase of a Client
691    ///
692    /// This is the time allowed for the client to establish a connection
693    /// and if the connection is not established within this time,
694    /// the client returns a timeout error.
695    ///
696    /// Timeout errors are retried, subject to the [`RetryConfig`]
697    ///
698    /// Default is 5 seconds
699    ///
700    /// # See Also
701    /// * [`Self::with_timeout`] to set a timeout for the overall request
702    /// * [`Self::with_connect_timeout_disabled`] to disable the connect timeout
703    ///
704    /// [`RetryConfig`]: crate::RetryConfig
705    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
706        self.connect_timeout = Some(ConfigValue::Parsed(timeout));
707        self
708    }
709
710    /// Disables the connection timeout
711    ///
712    /// # See Also
713    /// * [`Self::with_connect_timeout`]
714    pub fn with_connect_timeout_disabled(mut self) -> Self {
715        self.connect_timeout = None;
716        self
717    }
718
719    /// Set a read timeout
720    ///
721    /// The timeout applies to each read operation, and resets after a
722    /// successful read. This is useful for detecting stalled connections
723    /// when the size of the response is not known beforehand.
724    ///
725    /// Timeout errors are retried, subject to the [`RetryConfig`]
726    ///
727    /// Default is disabled (no read timeout)
728    ///
729    /// # See Also
730    /// * [`Self::with_read_timeout_disabled`] to disable the read timeout
731    /// * [`Self::with_timeout`] to set a timeout for the overall request
732    /// * [`Self::with_connect_timeout`] to set a timeout for the connect phase
733    ///
734    /// [`RetryConfig`]: crate::RetryConfig
735    pub fn with_read_timeout(mut self, timeout: Duration) -> Self {
736        self.read_timeout = Some(ConfigValue::Parsed(timeout));
737        self
738    }
739
740    /// Disables the read timeout
741    ///
742    /// # See Also
743    /// * [`Self::with_read_timeout`]
744    pub fn with_read_timeout_disabled(mut self) -> Self {
745        self.read_timeout = None;
746        self
747    }
748
749    /// Set the pool max idle timeout
750    ///
751    /// This is the length of time an idle connection will be kept alive
752    ///
753    /// Default is 90 seconds enforced by reqwest
754    pub fn with_pool_idle_timeout(mut self, timeout: Duration) -> Self {
755        self.pool_idle_timeout = Some(ConfigValue::Parsed(timeout));
756        self
757    }
758
759    /// Set the maximum number of idle connections per host
760    ///
761    /// Default is no limit enforced by reqwest
762    pub fn with_pool_max_idle_per_host(mut self, max: usize) -> Self {
763        self.pool_max_idle_per_host = Some(max.into());
764        self
765    }
766
767    /// Sets an interval for HTTP/2 Ping frames should be sent to keep a connection alive.
768    ///
769    /// Default is disabled enforced by reqwest
770    pub fn with_http2_keep_alive_interval(mut self, interval: Duration) -> Self {
771        self.http2_keep_alive_interval = Some(ConfigValue::Parsed(interval));
772        self
773    }
774
775    /// Sets a timeout for receiving an acknowledgement of the keep-alive ping.
776    ///
777    /// If the ping is not acknowledged within the timeout, the connection will be closed.
778    /// Does nothing if `http2_keep_alive_interval` is disabled.
779    ///
780    /// Default is disabled enforced by reqwest
781    pub fn with_http2_keep_alive_timeout(mut self, interval: Duration) -> Self {
782        self.http2_keep_alive_timeout = Some(ConfigValue::Parsed(interval));
783        self
784    }
785
786    /// Enable HTTP/2 keep alive pings for idle connections
787    ///
788    /// If disabled, keep-alive pings are only sent while there are open request/response
789    /// streams. If enabled, pings are also sent when no streams are active
790    ///
791    /// Default is disabled enforced by reqwest
792    pub fn with_http2_keep_alive_while_idle(mut self) -> Self {
793        self.http2_keep_alive_while_idle = true.into();
794        self
795    }
796
797    /// Sets the maximum frame size to use for HTTP/2.
798    ///
799    /// Default is currently 16,384 but may change internally to optimize for common uses.
800    pub fn with_http2_max_frame_size(mut self, sz: u32) -> Self {
801        self.http2_max_frame_size = Some(ConfigValue::Parsed(sz));
802        self
803    }
804
805    /// Override the default DNS resolution with a custom [`DnsResolver`]
806    ///
807    /// When set, [`ClientConfigKey::RandomizeAddresses`] is ignored: the
808    /// provided resolver is fully responsible for resolution, ordering,
809    /// shuffling, and caching.
810    ///
811    /// The built-in reqwest-based transport applies this automatically.
812    /// Custom [`HttpConnector`] implementations should read it via
813    /// [`Self::dns_resolver`] and honor it.
814    ///
815    /// Note: unlike other options, this cannot be configured via
816    /// [`ClientConfigKey`] / string configuration.
817    ///
818    /// [`HttpConnector`]: crate::client::HttpConnector
819    #[cfg(not(target_arch = "wasm32"))]
820    pub fn with_dns_resolver(mut self, resolver: Arc<dyn DnsResolver>) -> Self {
821        self.dns_resolver = Some(resolver);
822        self
823    }
824
825    /// Return the custom [`DnsResolver`] if any, see [`Self::with_dns_resolver`]
826    #[cfg(not(target_arch = "wasm32"))]
827    pub fn dns_resolver(&self) -> Option<&Arc<dyn DnsResolver>> {
828        self.dns_resolver.as_ref()
829    }
830
831    /// Get the default headers defined through `ClientOptions::with_default_headers`
832    pub fn get_default_headers(&self) -> Option<&HeaderMap> {
833        self.default_headers.as_ref()
834    }
835
836    /// Get the mime type for the file in `path` to be uploaded
837    ///
838    /// Gets the file extension from `path`, and returns the
839    /// mime type if it was defined initially through
840    /// `ClientOptions::with_content_type_for_suffix`
841    ///
842    /// Otherwise, returns the default mime type if it was defined
843    /// earlier through `ClientOptions::with_default_content_type`
844    pub fn get_content_type(&self, path: &Path) -> Option<&str> {
845        match path.extension() {
846            Some(extension) => match self.content_type_map.get(extension) {
847                Some(ct) => Some(ct.as_str()),
848                None => self.default_content_type.as_deref(),
849            },
850            None => self.default_content_type.as_deref(),
851        }
852    }
853
854    /// Returns a copy of this [`ClientOptions`] with overrides necessary for metadata endpoint access
855    ///
856    /// In particular:
857    /// * Allows HTTP as metadata endpoints do not use TLS
858    /// * Configures a low connection timeout to provide quick feedback if not present
859    #[cfg(any(feature = "aws-base", feature = "gcp-base", feature = "azure-base"))]
860    pub(crate) fn metadata_options(&self) -> Self {
861        self.clone()
862            .with_allow_http(true)
863            .with_connect_timeout(Duration::from_secs(1))
864    }
865
866    #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
867    pub(crate) fn client(&self) -> Result<reqwest::Client> {
868        let mut builder = reqwest::ClientBuilder::new();
869
870        match &self.user_agent {
871            Some(user_agent) => builder = builder.user_agent(user_agent.get()?),
872            None => builder = builder.user_agent(DEFAULT_USER_AGENT),
873        }
874
875        if let Some(headers) = &self.default_headers {
876            builder = builder.default_headers(headers.clone())
877        }
878
879        if let Some(proxy) = &self.proxy_url {
880            let mut proxy = Proxy::all(proxy).map_err(map_client_error)?;
881
882            if let Some(certificate) = &self.proxy_ca_certificate {
883                let certificate = reqwest::tls::Certificate::from_pem(certificate.as_bytes())
884                    .map_err(map_client_error)?;
885
886                builder = builder.tls_certs_merge(std::iter::once(certificate));
887            }
888
889            if let Some(proxy_excludes) = &self.proxy_excludes {
890                let no_proxy = NoProxy::from_string(proxy_excludes);
891
892                proxy = proxy.no_proxy(no_proxy);
893            }
894
895            builder = builder.proxy(proxy);
896        }
897
898        let certs = self
899            .root_certificates
900            .iter()
901            .map(|certificate| certificate.0.clone());
902
903        if self.no_system_certificates.get()? {
904            builder = builder.tls_certs_only(certs);
905        } else {
906            builder = builder.tls_certs_merge(certs);
907        }
908
909        if let Some(timeout) = &self.timeout {
910            builder = builder.timeout(timeout.get()?)
911        }
912
913        if let Some(timeout) = &self.connect_timeout {
914            builder = builder.connect_timeout(timeout.get()?)
915        }
916
917        if let Some(timeout) = &self.read_timeout {
918            builder = builder.read_timeout(timeout.get()?)
919        }
920
921        if let Some(timeout) = &self.pool_idle_timeout {
922            builder = builder.pool_idle_timeout(timeout.get()?)
923        }
924
925        if let Some(max) = &self.pool_max_idle_per_host {
926            builder = builder.pool_max_idle_per_host(max.get()?)
927        }
928
929        if let Some(interval) = &self.http2_keep_alive_interval {
930            builder = builder.http2_keep_alive_interval(interval.get()?)
931        }
932
933        if let Some(interval) = &self.http2_keep_alive_timeout {
934            builder = builder.http2_keep_alive_timeout(interval.get()?)
935        }
936
937        if self.http2_keep_alive_while_idle.get()? {
938            builder = builder.http2_keep_alive_while_idle(true)
939        }
940
941        if let Some(sz) = &self.http2_max_frame_size {
942            builder = builder.http2_max_frame_size(Some(sz.get()?))
943        }
944
945        if self.http1_only.get()? {
946            builder = builder.http1_only()
947        }
948
949        if self.http2_only.get()? {
950            builder = builder.http2_prior_knowledge()
951        }
952
953        if self.allow_invalid_certificates.get()? {
954            builder = builder.danger_accept_invalid_certs(true)
955        }
956
957        // Explicitly disable compression, since it may be automatically enabled
958        // when certain reqwest features are enabled. Compression interferes
959        // with the `Content-Length` header, which is used to determine the
960        // size of objects.
961        builder = builder.no_gzip().no_brotli().no_zstd().no_deflate();
962
963        let resolver: Option<Arc<dyn DnsResolver>> = match &self.dns_resolver {
964            Some(resolver) => Some(Arc::clone(resolver)),
965            None if self.randomize_addresses.get()? => Some(Arc::new(dns::ShuffleResolver)),
966            None => None,
967        };
968        if let Some(resolver) = resolver {
969            builder = builder.dns_resolver(Arc::new(dns::ReqwestResolver(resolver)));
970        }
971
972        builder
973            .https_only(!self.allow_http.get()?)
974            .build()
975            .map_err(map_client_error)
976    }
977
978    #[cfg(all(feature = "reqwest", target_arch = "wasm32", target_os = "unknown"))]
979    pub(crate) fn client(&self) -> Result<reqwest::Client> {
980        let mut builder = reqwest::ClientBuilder::new();
981
982        match &self.user_agent {
983            Some(user_agent) => builder = builder.user_agent(user_agent.get()?),
984            None => builder = builder.user_agent(DEFAULT_USER_AGENT),
985        }
986
987        if let Some(headers) = &self.default_headers {
988            builder = builder.default_headers(headers.clone())
989        }
990
991        builder.build().map_err(map_client_error)
992    }
993}
994
995pub(crate) trait GetOptionsExt {
996    fn with_get_options(self, options: GetOptions) -> Self;
997}
998
999impl GetOptionsExt for HttpRequestBuilder {
1000    fn with_get_options(mut self, options: GetOptions) -> Self {
1001        use hyper::header::*;
1002
1003        let GetOptions {
1004            if_match,
1005            if_none_match,
1006            if_modified_since,
1007            if_unmodified_since,
1008            range,
1009            version: _,
1010            head: _,
1011            extensions,
1012        } = options;
1013
1014        if let Some(range) = range {
1015            self = self.header(RANGE, range.to_string());
1016        }
1017
1018        if let Some(tag) = if_match {
1019            self = self.header(IF_MATCH, tag);
1020        }
1021
1022        if let Some(tag) = if_none_match {
1023            self = self.header(IF_NONE_MATCH, tag);
1024        }
1025
1026        const DATE_FORMAT: &str = "%a, %d %b %Y %H:%M:%S GMT";
1027        if let Some(date) = if_unmodified_since {
1028            self = self.header(IF_UNMODIFIED_SINCE, date.format(DATE_FORMAT).to_string());
1029        }
1030
1031        if let Some(date) = if_modified_since {
1032            self = self.header(IF_MODIFIED_SINCE, date.format(DATE_FORMAT).to_string());
1033        }
1034
1035        self = self.extensions(extensions);
1036
1037        self
1038    }
1039}
1040
1041/// Provides credentials for use when signing requests
1042#[async_trait]
1043pub trait CredentialProvider: std::fmt::Debug + Send + Sync {
1044    /// The type of credential returned by this provider
1045    type Credential;
1046
1047    /// Return a credential
1048    async fn get_credential(&self) -> Result<Arc<Self::Credential>>;
1049}
1050
1051/// A static set of credentials
1052#[derive(Debug)]
1053pub struct StaticCredentialProvider<T> {
1054    credential: Arc<T>,
1055}
1056
1057impl<T> StaticCredentialProvider<T> {
1058    /// A [`CredentialProvider`] for a static credential of type `T`
1059    pub fn new(credential: T) -> Self {
1060        Self {
1061            credential: Arc::new(credential),
1062        }
1063    }
1064}
1065
1066#[async_trait]
1067impl<T> CredentialProvider for StaticCredentialProvider<T>
1068where
1069    T: std::fmt::Debug + Send + Sync,
1070{
1071    type Credential = T;
1072
1073    async fn get_credential(&self) -> Result<Arc<T>> {
1074        Ok(Arc::clone(&self.credential))
1075    }
1076}
1077
1078#[cfg(any(feature = "aws-base", feature = "azure-base", feature = "gcp-base"))]
1079mod cloud {
1080    use super::*;
1081    use crate::RetryConfig;
1082    use crate::client::token::{TemporaryToken, TokenCache};
1083
1084    /// A [`CredentialProvider`] that uses [`HttpClient`] to fetch temporary tokens
1085    #[derive(Debug)]
1086    pub(crate) struct TokenCredentialProvider<T: TokenProvider> {
1087        inner: T,
1088        client: HttpClient,
1089        retry: RetryConfig,
1090        cache: TokenCache<Arc<T::Credential>>,
1091    }
1092
1093    impl<T: TokenProvider> TokenCredentialProvider<T> {
1094        pub(crate) fn new(inner: T, client: HttpClient, retry: RetryConfig) -> Self {
1095            Self {
1096                inner,
1097                client,
1098                retry,
1099                cache: Default::default(),
1100            }
1101        }
1102
1103        /// Override the minimum remaining TTL for a cached token to be used
1104        #[cfg(any(feature = "aws-base", feature = "gcp-base"))]
1105        pub(crate) fn with_min_ttl(mut self, min_ttl: Duration) -> Self {
1106            self.cache = self.cache.with_min_ttl(min_ttl);
1107            self
1108        }
1109    }
1110
1111    #[async_trait]
1112    impl<T: TokenProvider> CredentialProvider for TokenCredentialProvider<T> {
1113        type Credential = T::Credential;
1114
1115        async fn get_credential(&self) -> Result<Arc<Self::Credential>> {
1116            self.cache
1117                .get_or_insert_with(|| self.inner.fetch_token(&self.client, &self.retry))
1118                .await
1119        }
1120    }
1121
1122    #[async_trait]
1123    pub(crate) trait TokenProvider: std::fmt::Debug + Send + Sync {
1124        type Credential: std::fmt::Debug + Send + Sync;
1125
1126        async fn fetch_token(
1127            &self,
1128            client: &HttpClient,
1129            retry: &RetryConfig,
1130        ) -> Result<TemporaryToken<Arc<Self::Credential>>>;
1131    }
1132}
1133
1134use crate::client::builder::HttpRequestBuilder;
1135#[cfg(any(feature = "aws-base", feature = "azure-base", feature = "gcp-base"))]
1136pub(crate) use cloud::*;
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141    use std::collections::HashMap;
1142
1143    #[test]
1144    fn client_test_config_from_map() {
1145        let allow_http = "true".to_string();
1146        let allow_invalid_certificates = "false".to_string();
1147        let connect_timeout = "90 seconds".to_string();
1148        let default_content_type = "object_store:fake_default_content_type".to_string();
1149        let http1_only = "true".to_string();
1150        let http2_only = "false".to_string();
1151        let http2_keep_alive_interval = "90 seconds".to_string();
1152        let http2_keep_alive_timeout = "91 seconds".to_string();
1153        let http2_keep_alive_while_idle = "92 seconds".to_string();
1154        let http2_max_frame_size = "1337".to_string();
1155        let no_system_certificates = "true".to_string();
1156        let pool_idle_timeout = "93 seconds".to_string();
1157        let pool_max_idle_per_host = "94".to_string();
1158        let proxy_url = "https://fake_proxy_url".to_string();
1159        let read_timeout = "45 seconds".to_string();
1160        let timeout = "95 seconds".to_string();
1161        let user_agent = "object_store:fake_user_agent".to_string();
1162
1163        let options = HashMap::from([
1164            ("allow_http", allow_http.clone()),
1165            (
1166                "allow_invalid_certificates",
1167                allow_invalid_certificates.clone(),
1168            ),
1169            ("connect_timeout", connect_timeout.clone()),
1170            ("default_content_type", default_content_type.clone()),
1171            ("http1_only", http1_only.clone()),
1172            ("http2_only", http2_only.clone()),
1173            (
1174                "http2_keep_alive_interval",
1175                http2_keep_alive_interval.clone(),
1176            ),
1177            ("http2_keep_alive_timeout", http2_keep_alive_timeout.clone()),
1178            (
1179                "http2_keep_alive_while_idle",
1180                http2_keep_alive_while_idle.clone(),
1181            ),
1182            ("http2_max_frame_size", http2_max_frame_size.clone()),
1183            (
1184                "disable_system_certificates",
1185                no_system_certificates.clone(),
1186            ),
1187            ("pool_idle_timeout", pool_idle_timeout.clone()),
1188            ("pool_max_idle_per_host", pool_max_idle_per_host.clone()),
1189            ("proxy_url", proxy_url.clone()),
1190            ("read_timeout", read_timeout.clone()),
1191            ("timeout", timeout.clone()),
1192            ("user_agent", user_agent.clone()),
1193        ]);
1194
1195        let builder = options
1196            .into_iter()
1197            .fold(ClientOptions::new(), |builder, (key, value)| {
1198                builder.with_config(key.parse().unwrap(), value)
1199            });
1200
1201        assert_eq!(
1202            builder
1203                .get_config_value(&ClientConfigKey::AllowHttp)
1204                .unwrap(),
1205            allow_http
1206        );
1207        assert_eq!(
1208            builder
1209                .get_config_value(&ClientConfigKey::AllowInvalidCertificates)
1210                .unwrap(),
1211            allow_invalid_certificates
1212        );
1213        assert_eq!(
1214            builder
1215                .get_config_value(&ClientConfigKey::ConnectTimeout)
1216                .unwrap(),
1217            connect_timeout
1218        );
1219        assert_eq!(
1220            builder
1221                .get_config_value(&ClientConfigKey::DefaultContentType)
1222                .unwrap(),
1223            default_content_type
1224        );
1225        assert_eq!(
1226            builder
1227                .get_config_value(&ClientConfigKey::Http1Only)
1228                .unwrap(),
1229            http1_only
1230        );
1231        assert_eq!(
1232            builder
1233                .get_config_value(&ClientConfigKey::Http2Only)
1234                .unwrap(),
1235            http2_only
1236        );
1237        assert_eq!(
1238            builder
1239                .get_config_value(&ClientConfigKey::Http2KeepAliveInterval)
1240                .unwrap(),
1241            http2_keep_alive_interval
1242        );
1243        assert_eq!(
1244            builder
1245                .get_config_value(&ClientConfigKey::Http2KeepAliveTimeout)
1246                .unwrap(),
1247            http2_keep_alive_timeout
1248        );
1249        assert_eq!(
1250            builder
1251                .get_config_value(&ClientConfigKey::Http2KeepAliveWhileIdle)
1252                .unwrap(),
1253            http2_keep_alive_while_idle
1254        );
1255        assert_eq!(
1256            builder
1257                .get_config_value(&ClientConfigKey::Http2MaxFrameSize)
1258                .unwrap(),
1259            http2_max_frame_size
1260        );
1261        assert_eq!(
1262            builder
1263                .get_config_value(&ClientConfigKey::NoSystemCertificates)
1264                .unwrap(),
1265            no_system_certificates
1266        );
1267
1268        assert_eq!(
1269            builder
1270                .get_config_value(&ClientConfigKey::PoolIdleTimeout)
1271                .unwrap(),
1272            pool_idle_timeout
1273        );
1274        assert_eq!(
1275            builder
1276                .get_config_value(&ClientConfigKey::PoolMaxIdlePerHost)
1277                .unwrap(),
1278            pool_max_idle_per_host
1279        );
1280        assert_eq!(
1281            builder
1282                .get_config_value(&ClientConfigKey::ProxyUrl)
1283                .unwrap(),
1284            proxy_url
1285        );
1286        assert_eq!(
1287            builder
1288                .get_config_value(&ClientConfigKey::ReadTimeout)
1289                .unwrap(),
1290            read_timeout
1291        );
1292        assert_eq!(
1293            builder.get_config_value(&ClientConfigKey::Timeout).unwrap(),
1294            timeout
1295        );
1296        assert_eq!(
1297            builder
1298                .get_config_value(&ClientConfigKey::UserAgent)
1299                .unwrap(),
1300            user_agent
1301        );
1302    }
1303
1304    #[tokio::test]
1305    #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))]
1306    async fn test_custom_dns_resolver() {
1307        use crate::client::mock_server::MockServer;
1308        use std::net::{IpAddr, Ipv4Addr};
1309        use std::sync::atomic::{AtomicUsize, Ordering};
1310
1311        #[derive(Debug, Default)]
1312        struct CountingResolver(AtomicUsize);
1313
1314        impl DnsResolver for CountingResolver {
1315            fn resolve(&self, host: &str) -> DnsFuture {
1316                assert_eq!(host, "localhost");
1317                self.0.fetch_add(1, Ordering::SeqCst);
1318                Box::pin(async { Ok(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]) })
1319            }
1320        }
1321
1322        let server = MockServer::new().await;
1323        server.push(::http::Response::new("hello".to_string()));
1324
1325        let resolver = Arc::new(CountingResolver::default());
1326        let client = ClientOptions::new()
1327            .with_allow_http(true)
1328            .with_dns_resolver(Arc::clone(&resolver) as Arc<dyn DnsResolver>)
1329            .client()
1330            .unwrap();
1331
1332        // Use a hostname (not an IP literal) so resolution actually runs
1333        let parsed = url::Url::parse(server.url()).unwrap();
1334        let url = format!("http://localhost:{}/", parsed.port().unwrap());
1335        let resp = client.get(url).send().await.unwrap();
1336        assert!(resp.status().is_success());
1337        assert_eq!(resolver.0.load(Ordering::SeqCst), 1);
1338    }
1339}