Skip to main content

oxihttp_client/
client_builder.rs

1//! [`ClientBuilder`] and related HTTP/2 configuration types.
2//!
3//! Extracted from `lib.rs` to keep individual source files under the 2 000-line
4//! policy limit.  All public items are re-exported from the crate root so the
5//! external API is unchanged.
6
7use http::{HeaderMap, HeaderValue, Uri};
8use hyper_util::client::legacy::connect::HttpConnector;
9use hyper_util::client::legacy::Client as HyperClient;
10use hyper_util::rt::TokioExecutor;
11use oxihttp_core::OxiHttpError;
12use std::sync::Arc;
13use std::time::Duration;
14
15use crate::middleware::ClientMiddleware;
16use crate::proxy::{ProxyConnector, ProxyKind};
17use crate::redirect::RedirectPolicy;
18use crate::resolver::BoxResolver;
19use crate::retry::RetryPolicy;
20
21#[cfg(feature = "socks")]
22use crate::proxy::Socks5Connector;
23
24#[cfg(feature = "tls")]
25use crate::connector::OxiHttpsConnector;
26#[cfg(feature = "tls")]
27use crate::tls;
28
29// Re-export the Client struct and type aliases so build methods can reference them.
30use super::Client;
31#[cfg(feature = "tls")]
32use super::TlsRebuildConfig;
33
34// ---------------------------------------------------------------------------
35// Http2Settings
36// ---------------------------------------------------------------------------
37
38/// HTTP/2 connection settings for the client.
39#[derive(Debug, Clone, Default)]
40pub struct Http2Settings {
41    /// Initial window size for stream-level flow control (bytes).
42    pub initial_stream_window_size: Option<u32>,
43    /// Initial window size for connection-level flow control (bytes).
44    pub initial_connection_window_size: Option<u32>,
45    /// Enable adaptive flow control (overrides window size settings when set).
46    pub adaptive_window: Option<bool>,
47    /// Interval for HTTP/2 PING keep-alive frames.
48    pub keep_alive_interval: Option<std::time::Duration>,
49    /// Timeout for acknowledgement of keep-alive PING before closing.
50    pub keep_alive_timeout: Option<std::time::Duration>,
51    /// Maximum HTTP/2 frame size (bytes).
52    pub max_frame_size: Option<u32>,
53    /// Maximum number of concurrent locally-reset streams.
54    pub max_concurrent_reset_streams: Option<usize>,
55    /// Maximum write buffer size per HTTP/2 stream (bytes).
56    pub max_send_buf_size: Option<usize>,
57}
58
59// ---------------------------------------------------------------------------
60// apply_http2_settings — shared helper for all client build paths
61// ---------------------------------------------------------------------------
62
63pub(crate) fn apply_http2_settings(
64    builder: &mut hyper_util::client::legacy::Builder,
65    settings: &Http2Settings,
66) {
67    if let Some(sz) = settings.initial_stream_window_size {
68        builder.http2_initial_stream_window_size(sz);
69    }
70    if let Some(sz) = settings.initial_connection_window_size {
71        builder.http2_initial_connection_window_size(sz);
72    }
73    if let Some(adaptive) = settings.adaptive_window {
74        builder.http2_adaptive_window(adaptive);
75    }
76    if let Some(interval) = settings.keep_alive_interval {
77        builder.http2_keep_alive_interval(interval);
78    }
79    if let Some(timeout) = settings.keep_alive_timeout {
80        builder.http2_keep_alive_timeout(timeout);
81    }
82    if let Some(sz) = settings.max_frame_size {
83        builder.http2_max_frame_size(sz);
84    }
85    if let Some(n) = settings.max_concurrent_reset_streams {
86        builder.http2_max_concurrent_reset_streams(n);
87    }
88    if let Some(sz) = settings.max_send_buf_size {
89        builder.http2_max_send_buf_size(sz);
90    }
91}
92
93// ---------------------------------------------------------------------------
94// ClientBuilder
95// ---------------------------------------------------------------------------
96
97/// A builder for constructing a `Client` with custom configuration.
98pub struct ClientBuilder {
99    pub(super) pool_max_idle_per_host: Option<usize>,
100    pub(super) pool_idle_timeout: Option<Duration>,
101    pub(super) connect_timeout: Option<Duration>,
102    pub(super) read_timeout: Option<Duration>,
103    pub(super) redirect_policy: RedirectPolicy,
104    pub(super) retry_policy: Option<RetryPolicy>,
105    pub(super) default_headers: HeaderMap,
106    pub(super) user_agent: Option<String>,
107    pub(super) decompression: bool,
108    /// Middleware interceptors applied to every request/response.
109    pub(super) middleware: Vec<Arc<dyn ClientMiddleware>>,
110    /// Optional proxy configuration.
111    pub(super) proxy: Option<ProxyKind>,
112    /// Optional shared cookie jar for automatic cookie management.
113    pub(super) cookie_jar: Option<Arc<std::sync::Mutex<oxihttp_core::CookieJar>>>,
114    /// HTTP/2 tuning settings (applied to all build paths that support H2).
115    pub(super) http2_settings: Option<Http2Settings>,
116    /// TCP_NODELAY setting for all outgoing connections.
117    pub(super) tcp_nodelay: Option<bool>,
118    /// TCP keepalive idle time for all outgoing connections.
119    pub(super) tcp_keepalive: Option<Duration>,
120    /// Custom DNS resolver (used with build_with_resolver / build_https_with_resolver).
121    pub(super) resolver: Option<Arc<dyn crate::resolver::DnsResolver>>,
122    // TLS options (only active when `tls` feature is enabled)
123    #[cfg(feature = "tls")]
124    pub(super) trusted_certs_der: Vec<Vec<u8>>,
125    #[cfg(feature = "tls")]
126    pub(super) alpn: Vec<String>,
127    #[cfg(feature = "tls")]
128    pub(super) accept_invalid_certs: bool,
129    #[cfg(feature = "tls")]
130    pub(super) use_webpki_roots: bool,
131    /// Optional path for SSLKEYLOGFILE-style key logging (development/debugging only).
132    #[cfg(feature = "tls")]
133    pub(super) key_log_path: Option<std::path::PathBuf>,
134    /// Enable TLS 1.3 0-RTT early data (HTTP fast-open) for subsequent requests.
135    #[cfg(feature = "tls")]
136    pub(super) early_data: bool,
137    /// Optional custom server-certificate verifier.  When `Some`, takes
138    /// precedence over `accept_invalid_certs` and all trust-store settings.
139    #[cfg(feature = "tls")]
140    pub(super) custom_cert_verifier:
141        Option<std::sync::Arc<dyn rustls::client::danger::ServerCertVerifier>>,
142}
143
144impl ClientBuilder {
145    /// Create a new `ClientBuilder` with default settings.
146    pub fn new() -> Self {
147        Self {
148            pool_max_idle_per_host: None,
149            pool_idle_timeout: None,
150            connect_timeout: None,
151            read_timeout: None,
152            redirect_policy: RedirectPolicy::default(),
153            retry_policy: None,
154            default_headers: HeaderMap::new(),
155            user_agent: None,
156            decompression: false,
157            middleware: Vec::new(),
158            proxy: None,
159            cookie_jar: None,
160            http2_settings: None,
161            tcp_nodelay: None,
162            tcp_keepalive: None,
163            resolver: None,
164            #[cfg(feature = "tls")]
165            trusted_certs_der: Vec::new(),
166            #[cfg(feature = "tls")]
167            alpn: Vec::new(),
168            #[cfg(feature = "tls")]
169            accept_invalid_certs: false,
170            #[cfg(feature = "tls")]
171            use_webpki_roots: false,
172            #[cfg(feature = "tls")]
173            key_log_path: None,
174            #[cfg(feature = "tls")]
175            early_data: false,
176            #[cfg(feature = "tls")]
177            custom_cert_verifier: None,
178        }
179    }
180
181    /// Set the maximum number of idle connections per host in the pool.
182    pub fn pool_max_idle_per_host(mut self, n: usize) -> Self {
183        self.pool_max_idle_per_host = Some(n);
184        self
185    }
186
187    /// Set the idle timeout for pooled connections.
188    pub fn pool_idle_timeout(mut self, duration: Duration) -> Self {
189        self.pool_idle_timeout = Some(duration);
190        self
191    }
192
193    /// Set the TCP connect timeout.
194    pub fn connect_timeout(mut self, duration: Duration) -> Self {
195        self.connect_timeout = Some(duration);
196        self
197    }
198
199    /// Set the response read timeout.
200    pub fn read_timeout(mut self, duration: Duration) -> Self {
201        self.read_timeout = Some(duration);
202        self
203    }
204
205    /// Set the redirect policy.
206    pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
207        self.redirect_policy = policy;
208        self
209    }
210
211    /// Set the retry policy.
212    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
213        self.retry_policy = Some(policy);
214        self
215    }
216
217    /// Set default headers to include on every request.
218    pub fn default_headers(mut self, headers: HeaderMap) -> Self {
219        self.default_headers = headers;
220        self
221    }
222
223    /// Set the User-Agent header for all requests.
224    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
225        self.user_agent = Some(agent.into());
226        self
227    }
228
229    /// Enable or disable automatic response decompression.
230    ///
231    /// When enabled the client adds `Accept-Encoding: gzip, deflate` to
232    /// outgoing requests and transparently decompresses the response body
233    /// based on the `Content-Encoding` header (requires `decompression`
234    /// feature).
235    pub fn with_decompression(mut self, enabled: bool) -> Self {
236        self.decompression = enabled;
237        self
238    }
239
240    // --- middleware --------------------------------------------------------
241
242    /// Register a request/response middleware interceptor.
243    ///
244    /// Middleware is invoked in registration order: `before_request` fires
245    /// in FIFO order before the first network attempt; `after_response` fires
246    /// in FIFO order after a successful final response.
247    ///
248    /// Multiple calls to this method append to the middleware stack.
249    ///
250    /// # Example
251    ///
252    /// ```rust,no_run
253    /// use oxihttp_client::{Client, middleware::LoggingMiddleware};
254    ///
255    /// let client = Client::builder()
256    ///     .with_middleware(LoggingMiddleware::new("api"))
257    ///     .build()
258    ///     .expect("client build");
259    /// ```
260    pub fn with_middleware<M: ClientMiddleware + 'static>(mut self, m: M) -> Self {
261        self.middleware.push(Arc::new(m));
262        self
263    }
264
265    /// Alias for [`ClientBuilder::with_middleware`].
266    ///
267    /// Provided so that callers familiar with the `tower::Layer` terminology
268    /// can use the same name.  This does **not** accept a `tower::Layer`; for
269    /// a full tower `Service` composition see the `tower_compat` module docs.
270    pub fn with_layer<M: ClientMiddleware + 'static>(self, m: M) -> Self {
271        self.with_middleware(m)
272    }
273
274    // --- cookie jar builder methods ------------------------------------------
275
276    /// Configure the client to use the given shared cookie jar for automatic cookie management.
277    pub fn with_cookie_jar(mut self, jar: Arc<std::sync::Mutex<oxihttp_core::CookieJar>>) -> Self {
278        self.cookie_jar = Some(jar);
279        self
280    }
281
282    /// Configure the client to create and use a fresh cookie jar automatically.
283    pub fn with_new_cookie_jar(mut self) -> Self {
284        self.cookie_jar = Some(Arc::new(std::sync::Mutex::new(
285            oxihttp_core::CookieJar::new(),
286        )));
287        self
288    }
289
290    // --- HTTP/2 and TCP tuning builder methods --------------------------------
291
292    /// Set HTTP/2 connection tuning parameters.
293    pub fn with_http2_settings(mut self, settings: Http2Settings) -> Self {
294        self.http2_settings = Some(settings);
295        self
296    }
297
298    /// Enable or disable `TCP_NODELAY` on all outgoing connections.
299    pub fn with_tcp_nodelay(mut self, nodelay: bool) -> Self {
300        self.tcp_nodelay = Some(nodelay);
301        self
302    }
303
304    /// Set the TCP keepalive idle time for all outgoing connections.
305    pub fn with_tcp_keepalive(mut self, duration: Duration) -> Self {
306        self.tcp_keepalive = Some(duration);
307        self
308    }
309
310    // --- custom DNS resolver builder methods ---------------------------------
311
312    /// Set a custom DNS resolver for all connections made by this client.
313    ///
314    /// After calling this, use [`ClientBuilder::build_with_resolver`] (plain HTTP)
315    /// or [`ClientBuilder::build_https_with_resolver`] (TLS) to construct the client.
316    pub fn with_resolver<R: crate::resolver::DnsResolver>(mut self, r: R) -> Self {
317        self.resolver = Some(Arc::new(r));
318        self
319    }
320
321    // --- proxy builder methods -----------------------------------------------
322
323    /// Route all requests through an HTTP CONNECT proxy.
324    ///
325    /// Call `build_proxy()` (plain HTTP target) or `build_proxy_https()` (HTTPS
326    /// target, requires `tls` feature) after setting this.
327    pub fn with_http_proxy(mut self, uri: Uri) -> Self {
328        self.proxy = Some(ProxyKind::HttpConnect(uri));
329        self
330    }
331
332    /// Route all requests through a SOCKS5 proxy.
333    ///
334    /// Call `build_socks5_proxy()` (plain HTTP) or `build_socks5_proxy_https()`
335    /// (HTTPS, requires `tls` feature) after setting this.
336    #[cfg(feature = "socks")]
337    pub fn with_socks5_proxy(mut self, uri: Uri) -> Self {
338        self.proxy = Some(ProxyKind::Socks5(uri));
339        self
340    }
341
342    /// Build a plain-HTTP `Client` routed through an HTTP CONNECT proxy.
343    ///
344    /// `with_http_proxy()` must have been called before this.
345    pub fn build_proxy(self) -> Result<Client<ProxyConnector>, OxiHttpError> {
346        let proxy_uri = match self.proxy.as_ref() {
347            Some(ProxyKind::HttpConnect(u)) => u.clone(),
348            #[cfg(feature = "socks")]
349            Some(ProxyKind::Socks5(_)) => {
350                return Err(OxiHttpError::ConnectionPool(
351                    "SOCKS5 proxy configured; use build_socks5_proxy() instead".into(),
352                ))
353            }
354            None => {
355                return Err(OxiHttpError::ConnectionPool(
356                    "no proxy configured; call with_http_proxy() first".into(),
357                ))
358            }
359        };
360        let connector = ProxyConnector::new(proxy_uri, self.connect_timeout);
361        let mut builder = HyperClient::builder(TokioExecutor::new());
362        if let Some(n) = self.pool_max_idle_per_host {
363            builder.pool_max_idle_per_host(n);
364        }
365        if let Some(dur) = self.pool_idle_timeout {
366            builder.pool_idle_timeout(dur);
367        }
368        if let Some(ref h2) = self.http2_settings {
369            apply_http2_settings(&mut builder, h2);
370        }
371        let inner = builder.build(connector);
372        let mut default_headers = self.default_headers;
373        if let Some(agent) = &self.user_agent {
374            if let Ok(val) = HeaderValue::from_str(agent) {
375                default_headers.insert(http::header::USER_AGENT, val);
376            }
377        }
378        Ok(Client {
379            inner,
380            redirect_policy: self.redirect_policy,
381            retry_policy: self.retry_policy,
382            default_headers,
383            connect_timeout: self.connect_timeout,
384            read_timeout: self.read_timeout,
385            decompression: self.decompression,
386            middleware: self.middleware,
387            cookie_jar: self.cookie_jar.clone(),
388            #[cfg(feature = "tls")]
389            tls_rebuild: None,
390        })
391    }
392
393    /// Build a plain-HTTP `Client` routed through a SOCKS5 proxy.
394    ///
395    /// `with_socks5_proxy()` must have been called before this.
396    #[cfg(feature = "socks")]
397    pub fn build_socks5_proxy(self) -> Result<Client<Socks5Connector>, OxiHttpError> {
398        let proxy_uri = match self.proxy.as_ref() {
399            Some(ProxyKind::Socks5(u)) => u.clone(),
400            Some(ProxyKind::HttpConnect(_)) => {
401                return Err(OxiHttpError::ConnectionPool(
402                    "HTTP CONNECT proxy configured; use build_proxy() instead".into(),
403                ))
404            }
405            None => {
406                return Err(OxiHttpError::ConnectionPool(
407                    "no proxy configured; call with_socks5_proxy() first".into(),
408                ))
409            }
410        };
411        let connector = Socks5Connector::new(proxy_uri, self.connect_timeout);
412        let mut builder = HyperClient::builder(TokioExecutor::new());
413        if let Some(n) = self.pool_max_idle_per_host {
414            builder.pool_max_idle_per_host(n);
415        }
416        if let Some(dur) = self.pool_idle_timeout {
417            builder.pool_idle_timeout(dur);
418        }
419        if let Some(ref h2) = self.http2_settings {
420            apply_http2_settings(&mut builder, h2);
421        }
422        let inner = builder.build(connector);
423        let mut default_headers = self.default_headers;
424        if let Some(agent) = &self.user_agent {
425            if let Ok(val) = HeaderValue::from_str(agent) {
426                default_headers.insert(http::header::USER_AGENT, val);
427            }
428        }
429        Ok(Client {
430            inner,
431            redirect_policy: self.redirect_policy,
432            retry_policy: self.retry_policy,
433            default_headers,
434            connect_timeout: self.connect_timeout,
435            read_timeout: self.read_timeout,
436            decompression: self.decompression,
437            middleware: self.middleware,
438            cookie_jar: self.cookie_jar.clone(),
439            #[cfg(feature = "tls")]
440            tls_rebuild: None,
441        })
442    }
443
444    /// Build a TLS-capable `Client` that tunnels HTTPS through an HTTP CONNECT proxy.
445    ///
446    /// `with_http_proxy()` must have been called before this.
447    ///
448    /// The resulting client handles both `http://` and `https://` URIs.
449    #[cfg(feature = "tls")]
450    pub fn build_proxy_https(
451        self,
452    ) -> Result<Client<OxiHttpsConnector<ProxyConnector>>, OxiHttpError> {
453        let proxy_uri = match self.proxy.as_ref() {
454            Some(ProxyKind::HttpConnect(u)) => u.clone(),
455            #[cfg(feature = "socks")]
456            Some(ProxyKind::Socks5(_)) => {
457                return Err(OxiHttpError::ConnectionPool(
458                    "SOCKS5 proxy configured; use build_socks5_proxy_https() instead".into(),
459                ))
460            }
461            None => {
462                return Err(OxiHttpError::ConnectionPool(
463                    "no proxy configured; call with_http_proxy() first".into(),
464                ))
465            }
466        };
467
468        let tls_connector = self.build_tls_connector_inner()?;
469
470        let http_connector = ProxyConnector::new(proxy_uri, self.connect_timeout);
471        let https_connector = OxiHttpsConnector::new(http_connector, tls_connector);
472
473        let mut builder = HyperClient::builder(TokioExecutor::new());
474        if let Some(n) = self.pool_max_idle_per_host {
475            builder.pool_max_idle_per_host(n);
476        }
477        if let Some(dur) = self.pool_idle_timeout {
478            builder.pool_idle_timeout(dur);
479        }
480        if let Some(ref h2) = self.http2_settings {
481            apply_http2_settings(&mut builder, h2);
482        }
483        let inner = builder.build(https_connector);
484        let mut default_headers = self.default_headers;
485        if let Some(agent) = &self.user_agent {
486            if let Ok(val) = HeaderValue::from_str(agent) {
487                default_headers.insert(http::header::USER_AGENT, val);
488            }
489        }
490        Ok(Client {
491            inner,
492            redirect_policy: self.redirect_policy,
493            retry_policy: self.retry_policy,
494            default_headers,
495            connect_timeout: self.connect_timeout,
496            read_timeout: self.read_timeout,
497            decompression: self.decompression,
498            middleware: self.middleware,
499            cookie_jar: self.cookie_jar.clone(),
500            #[cfg(feature = "tls")]
501            tls_rebuild: None,
502        })
503    }
504
505    /// Build a TLS-capable `Client` that tunnels HTTPS through a SOCKS5 proxy.
506    ///
507    /// `with_socks5_proxy()` must have been called before this.
508    ///
509    /// The resulting client handles both `http://` and `https://` URIs.
510    #[cfg(all(feature = "tls", feature = "socks"))]
511    pub fn build_socks5_proxy_https(
512        self,
513    ) -> Result<Client<OxiHttpsConnector<Socks5Connector>>, OxiHttpError> {
514        let proxy_uri = match self.proxy.as_ref() {
515            Some(ProxyKind::Socks5(u)) => u.clone(),
516            Some(ProxyKind::HttpConnect(_)) => {
517                return Err(OxiHttpError::ConnectionPool(
518                    "HTTP CONNECT proxy configured; use build_proxy_https() instead".into(),
519                ))
520            }
521            None => {
522                return Err(OxiHttpError::ConnectionPool(
523                    "no proxy configured; call with_socks5_proxy() first".into(),
524                ))
525            }
526        };
527
528        let tls_connector = self.build_tls_connector_inner()?;
529
530        let socks_connector = Socks5Connector::new(proxy_uri, self.connect_timeout);
531        let https_connector = OxiHttpsConnector::new(socks_connector, tls_connector);
532
533        let mut builder = HyperClient::builder(TokioExecutor::new());
534        if let Some(n) = self.pool_max_idle_per_host {
535            builder.pool_max_idle_per_host(n);
536        }
537        if let Some(dur) = self.pool_idle_timeout {
538            builder.pool_idle_timeout(dur);
539        }
540        if let Some(ref h2) = self.http2_settings {
541            apply_http2_settings(&mut builder, h2);
542        }
543        let inner = builder.build(https_connector);
544        let mut default_headers = self.default_headers;
545        if let Some(agent) = &self.user_agent {
546            if let Ok(val) = HeaderValue::from_str(agent) {
547                default_headers.insert(http::header::USER_AGENT, val);
548            }
549        }
550        Ok(Client {
551            inner,
552            redirect_policy: self.redirect_policy,
553            retry_policy: self.retry_policy,
554            default_headers,
555            connect_timeout: self.connect_timeout,
556            read_timeout: self.read_timeout,
557            decompression: self.decompression,
558            middleware: self.middleware,
559            cookie_jar: self.cookie_jar.clone(),
560            #[cfg(feature = "tls")]
561            tls_rebuild: None,
562        })
563    }
564
565    // --- TLS-specific builder methods (feature-gated) -----------------------
566
567    /// Enable TLS with the Mozilla CA bundle (webpki-roots) as the trust store.
568    ///
569    /// Required to be called (or `with_webpki_roots` / `with_trusted_cert_der`)
570    /// before `build_https()` returns a usable client for real HTTPS endpoints.
571    #[cfg(feature = "tls")]
572    pub fn with_tls(mut self) -> Self {
573        self.use_webpki_roots = true;
574        self
575    }
576
577    /// Trust the Mozilla CA bundle (webpki-roots).
578    #[cfg(feature = "tls")]
579    pub fn with_webpki_roots(mut self) -> Self {
580        self.use_webpki_roots = true;
581        self
582    }
583
584    /// Trust an additional DER-encoded CA certificate.
585    ///
586    /// Can be called multiple times to add several trusted roots.
587    #[cfg(feature = "tls")]
588    pub fn with_trusted_cert_der(mut self, der: Vec<u8>) -> Self {
589        self.trusted_certs_der.push(der);
590        self
591    }
592
593    /// Set ALPN protocols to advertise (e.g. `&["h2", "http/1.1"]`).
594    #[cfg(feature = "tls")]
595    pub fn with_alpn(mut self, protocols: &[&str]) -> Self {
596        self.alpn = protocols.iter().map(|s| s.to_string()).collect();
597        self
598    }
599
600    /// **DANGER**: Accept any server certificate, including self-signed ones.
601    ///
602    /// For testing only — disables all certificate verification.
603    #[cfg(feature = "tls")]
604    pub fn with_danger_accept_invalid_certs(mut self) -> Self {
605        self.accept_invalid_certs = true;
606        self
607    }
608
609    /// Write TLS session secrets to `path` in NSS key-log format (SSLKEYLOGFILE).
610    ///
611    /// The file is created/appended to on every TLS handshake. Use this for
612    /// decrypting captured HTTPS traffic in Wireshark or mitmproxy during
613    /// development. **Do not enable in production.**
614    #[cfg(feature = "tls")]
615    pub fn with_key_log_file(mut self, path: std::path::PathBuf) -> Self {
616        self.key_log_path = Some(path);
617        self
618    }
619
620    /// Enable TLS 1.3 0-RTT early data (HTTP fast-open) for subsequent requests.
621    ///
622    /// Only effective if a prior connection stored a session ticket and the server
623    /// indicated `max_early_data_size > 0`. Safe to call on any builder; ignored
624    /// if 0-RTT is not available.
625    ///
626    /// # Security
627    /// Early data is NOT protected against replay attacks — see RFC 8446 §8.
628    /// Only enable for idempotent requests (GET, HEAD, etc.).
629    #[cfg(feature = "tls")]
630    pub fn with_early_data(mut self) -> Self {
631        self.early_data = true;
632        self
633    }
634
635    /// **DANGER**: Enable or disable certificate verification via a boolean flag.
636    ///
637    /// This is an alias for [`with_danger_accept_invalid_certs`](Self::with_danger_accept_invalid_certs)
638    /// that accepts an explicit `bool` parameter, matching the API style of
639    /// reqwest's `danger_accept_invalid_certs`.
640    ///
641    /// # Security
642    ///
643    /// Passing `true` disables **all** TLS certificate verification, making HTTPS
644    /// connections trivially vulnerable to man-in-the-middle attacks.  Only use
645    /// in tests or isolated local environments.
646    ///
647    /// # Example
648    ///
649    /// ```rust,no_run
650    /// # fn example() -> Result<(), oxihttp_core::OxiHttpError> {
651    /// use oxihttp_client::Client;
652    ///
653    /// // Mirror reqwest's API: danger_accept_invalid_certs(true)
654    /// let client = Client::builder()
655    ///     .danger_accept_invalid_certs(true)
656    ///     .build_https()?;
657    /// # Ok(())
658    /// # }
659    /// ```
660    #[cfg(feature = "tls")]
661    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
662        self.accept_invalid_certs = accept;
663        self
664    }
665
666    /// Inject a custom server-certificate verifier.
667    ///
668    /// The supplied `verifier` replaces the default trust-store verification
669    /// for all TLS connections made by the built client.  When a custom verifier
670    /// is present it takes precedence over:
671    /// - `with_trusted_cert_der` / `with_webpki_roots`
672    /// - `danger_accept_invalid_certs` / `with_danger_accept_invalid_certs`
673    ///
674    /// This enables certificate pinning, custom CA hierarchies, or completely
675    /// bespoke verification logic without forking the library.
676    ///
677    /// # Security
678    ///
679    /// The security of the resulting client depends entirely on the supplied
680    /// verifier.  Injecting a verifier that accepts any certificate (e.g.
681    /// [`crate::tls::DangerousNoVerification`]) disables authentication;
682    /// see that type's documentation for details.
683    ///
684    /// # Example
685    ///
686    /// ```rust,no_run
687    /// # fn example() -> Result<(), oxihttp_core::OxiHttpError> {
688    /// use std::sync::Arc;
689    /// use oxihttp_client::{Client, tls::DangerousNoVerification};
690    ///
691    /// // Inject the "accept-everything" verifier (for tests only).
692    /// let provider = oxitls::pure_provider();
693    /// let verifier = Arc::new(DangerousNoVerification::new(provider));
694    ///
695    /// let client = Client::builder()
696    ///     .with_custom_cert_verifier(verifier)
697    ///     .build_https()?;
698    /// # Ok(())
699    /// # }
700    /// ```
701    #[cfg(feature = "tls")]
702    pub fn with_custom_cert_verifier(
703        mut self,
704        verifier: std::sync::Arc<dyn rustls::client::danger::ServerCertVerifier>,
705    ) -> Self {
706        self.custom_cert_verifier = Some(verifier);
707        self
708    }
709
710    // --- internal TLS connector helper (feature-gated) ----------------------
711
712    /// Choose the appropriate TLS connector builder depending on whether a
713    /// custom verifier has been injected.  This keeps the dispatch in one place
714    /// so all `build_*` variants stay consistent.
715    #[cfg(feature = "tls")]
716    fn build_tls_connector_inner(&self) -> Result<tokio_rustls::TlsConnector, OxiHttpError> {
717        if let Some(ref verifier) = self.custom_cert_verifier {
718            tls::build_tls_connector_with_verifier(
719                std::sync::Arc::clone(verifier),
720                &self.alpn,
721                self.early_data,
722            )
723        } else {
724            tls::build_tls_connector(
725                &self.trusted_certs_der,
726                &self.alpn,
727                self.accept_invalid_certs,
728                self.use_webpki_roots,
729                self.key_log_path.clone(),
730                self.early_data,
731            )
732        }
733    }
734
735    // --- build() — plain HTTP -----------------------------------------------
736
737    /// Build a plain HTTP `Client` (no TLS).
738    pub fn build(self) -> Result<Client, OxiHttpError> {
739        let mut builder = HyperClient::builder(TokioExecutor::new());
740
741        if let Some(n) = self.pool_max_idle_per_host {
742            builder.pool_max_idle_per_host(n);
743        }
744        if let Some(dur) = self.pool_idle_timeout {
745            builder.pool_idle_timeout(dur);
746        }
747        if let Some(ref h2) = self.http2_settings {
748            apply_http2_settings(&mut builder, h2);
749        }
750
751        // Use an explicit HttpConnector so TCP options can be applied.
752        let mut http = HttpConnector::new();
753        http.enforce_http(false);
754        if let Some(dur) = self.connect_timeout {
755            http.set_connect_timeout(Some(dur));
756        }
757        if let Some(nodelay) = self.tcp_nodelay {
758            http.set_nodelay(nodelay);
759        }
760        if let Some(ka) = self.tcp_keepalive {
761            http.set_keepalive(Some(ka));
762        }
763
764        let inner = builder.build(http);
765
766        let mut default_headers = self.default_headers;
767        if let Some(agent) = &self.user_agent {
768            if let Ok(val) = HeaderValue::from_str(agent) {
769                default_headers.insert(http::header::USER_AGENT, val);
770            }
771        }
772
773        Ok(Client {
774            inner,
775            redirect_policy: self.redirect_policy,
776            retry_policy: self.retry_policy,
777            default_headers,
778            connect_timeout: self.connect_timeout,
779            read_timeout: self.read_timeout,
780            decompression: self.decompression,
781            middleware: self.middleware,
782            cookie_jar: self.cookie_jar.clone(),
783            #[cfg(feature = "tls")]
784            tls_rebuild: None,
785        })
786    }
787
788    // --- build_https() — TLS-capable client ---------------------------------
789
790    /// Build a TLS-capable `HttpsClient` (alias for `Client<OxiHttpsConnector<HttpConnector>>`).
791    ///
792    /// The resulting client handles both `http://` and `https://` URIs.
793    #[cfg(feature = "tls")]
794    pub fn build_https(self) -> Result<super::HttpsClient, OxiHttpError> {
795        let connector = self.build_tls_connector_inner()?;
796
797        let mut http = HttpConnector::new();
798        // Allow the inner connector to accept https:// URIs so that the
799        // OxiHttpsConnector can extract the host/port and then upgrade the TCP
800        // stream to TLS.  Without this flag, HttpConnector rejects any URI
801        // whose scheme is not "http".
802        http.enforce_http(false);
803        if let Some(dur) = self.connect_timeout {
804            http.set_connect_timeout(Some(dur));
805        }
806        if let Some(nodelay) = self.tcp_nodelay {
807            http.set_nodelay(nodelay);
808        }
809        if let Some(ka) = self.tcp_keepalive {
810            http.set_keepalive(Some(ka));
811        }
812        let https_connector = OxiHttpsConnector::new(http, connector);
813
814        let mut builder = HyperClient::builder(TokioExecutor::new());
815
816        if let Some(n) = self.pool_max_idle_per_host {
817            builder.pool_max_idle_per_host(n);
818        }
819        if let Some(dur) = self.pool_idle_timeout {
820            builder.pool_idle_timeout(dur);
821        }
822        if let Some(ref h2) = self.http2_settings {
823            apply_http2_settings(&mut builder, h2);
824        }
825
826        let inner = builder.build(https_connector);
827
828        let mut default_headers = self.default_headers;
829        if let Some(agent) = &self.user_agent {
830            if let Ok(val) = HeaderValue::from_str(agent) {
831                default_headers.insert(http::header::USER_AGENT, val);
832            }
833        }
834
835        let tls_rebuild = Arc::new(TlsRebuildConfig {
836            trusted_certs_der: self.trusted_certs_der.clone(),
837            alpn: self.alpn.clone(),
838            accept_invalid_certs: self.accept_invalid_certs,
839            use_webpki_roots: self.use_webpki_roots,
840            key_log_path: self.key_log_path.clone(),
841            early_data: self.early_data,
842            connect_timeout: self.connect_timeout,
843            tcp_nodelay: self.tcp_nodelay,
844            tcp_keepalive: self.tcp_keepalive,
845            http2_settings: self.http2_settings.clone(),
846            pool_max_idle_per_host: self.pool_max_idle_per_host,
847            pool_idle_timeout: self.pool_idle_timeout,
848            custom_cert_verifier: self.custom_cert_verifier,
849        });
850
851        Ok(Client {
852            inner,
853            redirect_policy: self.redirect_policy,
854            retry_policy: self.retry_policy,
855            default_headers,
856            connect_timeout: self.connect_timeout,
857            read_timeout: self.read_timeout,
858            decompression: self.decompression,
859            middleware: self.middleware,
860            cookie_jar: self.cookie_jar.clone(),
861            tls_rebuild: Some(tls_rebuild),
862        })
863    }
864
865    // --- build_with_resolver / build_https_with_resolver ---------------------
866
867    /// Build a plain HTTP `Client` using a custom DNS resolver.
868    ///
869    /// [`ClientBuilder::with_resolver`] must be called before this.
870    pub fn build_with_resolver(self) -> Result<super::ResolverClient, OxiHttpError> {
871        let resolver = self.resolver.ok_or_else(|| {
872            OxiHttpError::Dns("with_resolver must be called before build_with_resolver".into())
873        })?;
874        let mut http = HttpConnector::new_with_resolver(BoxResolver(resolver));
875        http.enforce_http(false);
876        if let Some(dur) = self.connect_timeout {
877            http.set_connect_timeout(Some(dur));
878        }
879        if let Some(nodelay) = self.tcp_nodelay {
880            http.set_nodelay(nodelay);
881        }
882        if let Some(ka) = self.tcp_keepalive {
883            http.set_keepalive(Some(ka));
884        }
885
886        let mut builder = HyperClient::builder(TokioExecutor::new());
887        if let Some(n) = self.pool_max_idle_per_host {
888            builder.pool_max_idle_per_host(n);
889        }
890        if let Some(d) = self.pool_idle_timeout {
891            builder.pool_idle_timeout(d);
892        }
893        if let Some(ref h2) = self.http2_settings {
894            apply_http2_settings(&mut builder, h2);
895        }
896
897        let mut default_headers = self.default_headers;
898        if let Some(agent) = &self.user_agent {
899            if let Ok(val) = HeaderValue::from_str(agent) {
900                default_headers.insert(http::header::USER_AGENT, val);
901            }
902        }
903
904        Ok(Client {
905            inner: builder.build(http),
906            connect_timeout: self.connect_timeout,
907            read_timeout: self.read_timeout,
908            redirect_policy: self.redirect_policy,
909            retry_policy: self.retry_policy,
910            default_headers,
911            decompression: self.decompression,
912            middleware: self.middleware,
913            cookie_jar: self.cookie_jar,
914            #[cfg(feature = "tls")]
915            tls_rebuild: None,
916        })
917    }
918
919    /// Build a TLS-capable `Client` using a custom DNS resolver.
920    ///
921    /// [`ClientBuilder::with_resolver`] must be called before this.
922    /// The resulting client handles both `http://` and `https://` URIs.
923    #[cfg(feature = "tls")]
924    pub fn build_https_with_resolver(self) -> Result<super::ResolverHttpsClient, OxiHttpError> {
925        // Build the TLS connector first (while `self` is still whole), then
926        // extract the resolver field.  Reversing the order would cause a
927        // partial-move conflict because `ok_or_else` consumes `self.resolver`.
928        let tls_connector = self.build_tls_connector_inner()?;
929
930        let resolver = self.resolver.ok_or_else(|| {
931            OxiHttpError::Dns(
932                "with_resolver must be called before build_https_with_resolver".into(),
933            )
934        })?;
935
936        let mut http = HttpConnector::new_with_resolver(BoxResolver(resolver));
937        http.enforce_http(false);
938        if let Some(dur) = self.connect_timeout {
939            http.set_connect_timeout(Some(dur));
940        }
941        if let Some(nodelay) = self.tcp_nodelay {
942            http.set_nodelay(nodelay);
943        }
944        if let Some(ka) = self.tcp_keepalive {
945            http.set_keepalive(Some(ka));
946        }
947
948        let connector = crate::connector::OxiHttpsConnector::new(http, tls_connector);
949
950        let mut builder = HyperClient::builder(TokioExecutor::new());
951        if let Some(n) = self.pool_max_idle_per_host {
952            builder.pool_max_idle_per_host(n);
953        }
954        if let Some(d) = self.pool_idle_timeout {
955            builder.pool_idle_timeout(d);
956        }
957        if let Some(ref h2) = self.http2_settings {
958            apply_http2_settings(&mut builder, h2);
959        }
960
961        let mut default_headers = self.default_headers;
962        if let Some(agent) = &self.user_agent {
963            if let Ok(val) = HeaderValue::from_str(agent) {
964                default_headers.insert(http::header::USER_AGENT, val);
965            }
966        }
967
968        Ok(Client {
969            inner: builder.build(connector),
970            connect_timeout: self.connect_timeout,
971            read_timeout: self.read_timeout,
972            redirect_policy: self.redirect_policy,
973            retry_policy: self.retry_policy,
974            default_headers,
975            decompression: self.decompression,
976            middleware: self.middleware,
977            cookie_jar: self.cookie_jar,
978            #[cfg(feature = "tls")]
979            tls_rebuild: None,
980        })
981    }
982}
983
984impl Default for ClientBuilder {
985    fn default() -> Self {
986        Self::new()
987    }
988}
989
990impl std::fmt::Debug for ClientBuilder {
991    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
992        f.debug_struct("ClientBuilder")
993            .field("pool_max_idle_per_host", &self.pool_max_idle_per_host)
994            .field("pool_idle_timeout", &self.pool_idle_timeout)
995            .field("connect_timeout", &self.connect_timeout)
996            .field("read_timeout", &self.read_timeout)
997            .field("redirect_policy", &self.redirect_policy)
998            .field("retry_policy", &self.retry_policy)
999            .field("decompression", &self.decompression)
1000            .field("user_agent", &self.user_agent)
1001            .field("tcp_nodelay", &self.tcp_nodelay)
1002            .field("tcp_keepalive", &self.tcp_keepalive)
1003            .finish_non_exhaustive()
1004    }
1005}