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}
138
139impl ClientBuilder {
140    /// Create a new `ClientBuilder` with default settings.
141    pub fn new() -> Self {
142        Self {
143            pool_max_idle_per_host: None,
144            pool_idle_timeout: None,
145            connect_timeout: None,
146            read_timeout: None,
147            redirect_policy: RedirectPolicy::default(),
148            retry_policy: None,
149            default_headers: HeaderMap::new(),
150            user_agent: None,
151            decompression: false,
152            middleware: Vec::new(),
153            proxy: None,
154            cookie_jar: None,
155            http2_settings: None,
156            tcp_nodelay: None,
157            tcp_keepalive: None,
158            resolver: None,
159            #[cfg(feature = "tls")]
160            trusted_certs_der: Vec::new(),
161            #[cfg(feature = "tls")]
162            alpn: Vec::new(),
163            #[cfg(feature = "tls")]
164            accept_invalid_certs: false,
165            #[cfg(feature = "tls")]
166            use_webpki_roots: false,
167            #[cfg(feature = "tls")]
168            key_log_path: None,
169            #[cfg(feature = "tls")]
170            early_data: false,
171        }
172    }
173
174    /// Set the maximum number of idle connections per host in the pool.
175    pub fn pool_max_idle_per_host(mut self, n: usize) -> Self {
176        self.pool_max_idle_per_host = Some(n);
177        self
178    }
179
180    /// Set the idle timeout for pooled connections.
181    pub fn pool_idle_timeout(mut self, duration: Duration) -> Self {
182        self.pool_idle_timeout = Some(duration);
183        self
184    }
185
186    /// Set the TCP connect timeout.
187    pub fn connect_timeout(mut self, duration: Duration) -> Self {
188        self.connect_timeout = Some(duration);
189        self
190    }
191
192    /// Set the response read timeout.
193    pub fn read_timeout(mut self, duration: Duration) -> Self {
194        self.read_timeout = Some(duration);
195        self
196    }
197
198    /// Set the redirect policy.
199    pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
200        self.redirect_policy = policy;
201        self
202    }
203
204    /// Set the retry policy.
205    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
206        self.retry_policy = Some(policy);
207        self
208    }
209
210    /// Set default headers to include on every request.
211    pub fn default_headers(mut self, headers: HeaderMap) -> Self {
212        self.default_headers = headers;
213        self
214    }
215
216    /// Set the User-Agent header for all requests.
217    pub fn user_agent(mut self, agent: impl Into<String>) -> Self {
218        self.user_agent = Some(agent.into());
219        self
220    }
221
222    /// Enable or disable automatic response decompression.
223    ///
224    /// When enabled the client adds `Accept-Encoding: gzip, deflate` to
225    /// outgoing requests and transparently decompresses the response body
226    /// based on the `Content-Encoding` header (requires `decompression`
227    /// feature).
228    pub fn with_decompression(mut self, enabled: bool) -> Self {
229        self.decompression = enabled;
230        self
231    }
232
233    // --- middleware --------------------------------------------------------
234
235    /// Register a request/response middleware interceptor.
236    ///
237    /// Middleware is invoked in registration order: `before_request` fires
238    /// in FIFO order before the first network attempt; `after_response` fires
239    /// in FIFO order after a successful final response.
240    ///
241    /// Multiple calls to this method append to the middleware stack.
242    ///
243    /// # Example
244    ///
245    /// ```rust,no_run
246    /// use oxihttp_client::{Client, middleware::LoggingMiddleware};
247    ///
248    /// let client = Client::builder()
249    ///     .with_middleware(LoggingMiddleware::new("api"))
250    ///     .build()
251    ///     .expect("client build");
252    /// ```
253    pub fn with_middleware<M: ClientMiddleware + 'static>(mut self, m: M) -> Self {
254        self.middleware.push(Arc::new(m));
255        self
256    }
257
258    /// Alias for [`ClientBuilder::with_middleware`].
259    ///
260    /// Provided so that callers familiar with the `tower::Layer` terminology
261    /// can use the same name.  This does **not** accept a `tower::Layer`; for
262    /// a full tower `Service` composition see the `tower_compat` module docs.
263    pub fn with_layer<M: ClientMiddleware + 'static>(self, m: M) -> Self {
264        self.with_middleware(m)
265    }
266
267    // --- cookie jar builder methods ------------------------------------------
268
269    /// Configure the client to use the given shared cookie jar for automatic cookie management.
270    pub fn with_cookie_jar(mut self, jar: Arc<std::sync::Mutex<oxihttp_core::CookieJar>>) -> Self {
271        self.cookie_jar = Some(jar);
272        self
273    }
274
275    /// Configure the client to create and use a fresh cookie jar automatically.
276    pub fn with_new_cookie_jar(mut self) -> Self {
277        self.cookie_jar = Some(Arc::new(std::sync::Mutex::new(
278            oxihttp_core::CookieJar::new(),
279        )));
280        self
281    }
282
283    // --- HTTP/2 and TCP tuning builder methods --------------------------------
284
285    /// Set HTTP/2 connection tuning parameters.
286    pub fn with_http2_settings(mut self, settings: Http2Settings) -> Self {
287        self.http2_settings = Some(settings);
288        self
289    }
290
291    /// Enable or disable `TCP_NODELAY` on all outgoing connections.
292    pub fn with_tcp_nodelay(mut self, nodelay: bool) -> Self {
293        self.tcp_nodelay = Some(nodelay);
294        self
295    }
296
297    /// Set the TCP keepalive idle time for all outgoing connections.
298    pub fn with_tcp_keepalive(mut self, duration: Duration) -> Self {
299        self.tcp_keepalive = Some(duration);
300        self
301    }
302
303    // --- custom DNS resolver builder methods ---------------------------------
304
305    /// Set a custom DNS resolver for all connections made by this client.
306    ///
307    /// After calling this, use [`ClientBuilder::build_with_resolver`] (plain HTTP)
308    /// or [`ClientBuilder::build_https_with_resolver`] (TLS) to construct the client.
309    pub fn with_resolver<R: crate::resolver::DnsResolver>(mut self, r: R) -> Self {
310        self.resolver = Some(Arc::new(r));
311        self
312    }
313
314    // --- proxy builder methods -----------------------------------------------
315
316    /// Route all requests through an HTTP CONNECT proxy.
317    ///
318    /// Call `build_proxy()` (plain HTTP target) or `build_proxy_https()` (HTTPS
319    /// target, requires `tls` feature) after setting this.
320    pub fn with_http_proxy(mut self, uri: Uri) -> Self {
321        self.proxy = Some(ProxyKind::HttpConnect(uri));
322        self
323    }
324
325    /// Route all requests through a SOCKS5 proxy.
326    ///
327    /// Call `build_socks5_proxy()` (plain HTTP) or `build_socks5_proxy_https()`
328    /// (HTTPS, requires `tls` feature) after setting this.
329    #[cfg(feature = "socks")]
330    pub fn with_socks5_proxy(mut self, uri: Uri) -> Self {
331        self.proxy = Some(ProxyKind::Socks5(uri));
332        self
333    }
334
335    /// Build a plain-HTTP `Client` routed through an HTTP CONNECT proxy.
336    ///
337    /// `with_http_proxy()` must have been called before this.
338    pub fn build_proxy(self) -> Result<Client<ProxyConnector>, OxiHttpError> {
339        let proxy_uri = match self.proxy.as_ref() {
340            Some(ProxyKind::HttpConnect(u)) => u.clone(),
341            #[cfg(feature = "socks")]
342            Some(ProxyKind::Socks5(_)) => {
343                return Err(OxiHttpError::ConnectionPool(
344                    "SOCKS5 proxy configured; use build_socks5_proxy() instead".into(),
345                ))
346            }
347            None => {
348                return Err(OxiHttpError::ConnectionPool(
349                    "no proxy configured; call with_http_proxy() first".into(),
350                ))
351            }
352        };
353        let connector = ProxyConnector::new(proxy_uri, self.connect_timeout);
354        let mut builder = HyperClient::builder(TokioExecutor::new());
355        if let Some(n) = self.pool_max_idle_per_host {
356            builder.pool_max_idle_per_host(n);
357        }
358        if let Some(dur) = self.pool_idle_timeout {
359            builder.pool_idle_timeout(dur);
360        }
361        if let Some(ref h2) = self.http2_settings {
362            apply_http2_settings(&mut builder, h2);
363        }
364        let inner = builder.build(connector);
365        let mut default_headers = self.default_headers;
366        if let Some(agent) = &self.user_agent {
367            if let Ok(val) = HeaderValue::from_str(agent) {
368                default_headers.insert(http::header::USER_AGENT, val);
369            }
370        }
371        Ok(Client {
372            inner,
373            redirect_policy: self.redirect_policy,
374            retry_policy: self.retry_policy,
375            default_headers,
376            connect_timeout: self.connect_timeout,
377            read_timeout: self.read_timeout,
378            decompression: self.decompression,
379            middleware: self.middleware,
380            cookie_jar: self.cookie_jar.clone(),
381            #[cfg(feature = "tls")]
382            tls_rebuild: None,
383        })
384    }
385
386    /// Build a plain-HTTP `Client` routed through a SOCKS5 proxy.
387    ///
388    /// `with_socks5_proxy()` must have been called before this.
389    #[cfg(feature = "socks")]
390    pub fn build_socks5_proxy(self) -> Result<Client<Socks5Connector>, OxiHttpError> {
391        let proxy_uri = match self.proxy.as_ref() {
392            Some(ProxyKind::Socks5(u)) => u.clone(),
393            Some(ProxyKind::HttpConnect(_)) => {
394                return Err(OxiHttpError::ConnectionPool(
395                    "HTTP CONNECT proxy configured; use build_proxy() instead".into(),
396                ))
397            }
398            None => {
399                return Err(OxiHttpError::ConnectionPool(
400                    "no proxy configured; call with_socks5_proxy() first".into(),
401                ))
402            }
403        };
404        let connector = Socks5Connector::new(proxy_uri, self.connect_timeout);
405        let mut builder = HyperClient::builder(TokioExecutor::new());
406        if let Some(n) = self.pool_max_idle_per_host {
407            builder.pool_max_idle_per_host(n);
408        }
409        if let Some(dur) = self.pool_idle_timeout {
410            builder.pool_idle_timeout(dur);
411        }
412        if let Some(ref h2) = self.http2_settings {
413            apply_http2_settings(&mut builder, h2);
414        }
415        let inner = builder.build(connector);
416        let mut default_headers = self.default_headers;
417        if let Some(agent) = &self.user_agent {
418            if let Ok(val) = HeaderValue::from_str(agent) {
419                default_headers.insert(http::header::USER_AGENT, val);
420            }
421        }
422        Ok(Client {
423            inner,
424            redirect_policy: self.redirect_policy,
425            retry_policy: self.retry_policy,
426            default_headers,
427            connect_timeout: self.connect_timeout,
428            read_timeout: self.read_timeout,
429            decompression: self.decompression,
430            middleware: self.middleware,
431            cookie_jar: self.cookie_jar.clone(),
432            #[cfg(feature = "tls")]
433            tls_rebuild: None,
434        })
435    }
436
437    /// Build a TLS-capable `Client` that tunnels HTTPS through an HTTP CONNECT proxy.
438    ///
439    /// `with_http_proxy()` must have been called before this.
440    ///
441    /// The resulting client handles both `http://` and `https://` URIs.
442    #[cfg(feature = "tls")]
443    pub fn build_proxy_https(
444        self,
445    ) -> Result<Client<OxiHttpsConnector<ProxyConnector>>, OxiHttpError> {
446        let proxy_uri = match self.proxy.as_ref() {
447            Some(ProxyKind::HttpConnect(u)) => u.clone(),
448            #[cfg(feature = "socks")]
449            Some(ProxyKind::Socks5(_)) => {
450                return Err(OxiHttpError::ConnectionPool(
451                    "SOCKS5 proxy configured; use build_socks5_proxy_https() instead".into(),
452                ))
453            }
454            None => {
455                return Err(OxiHttpError::ConnectionPool(
456                    "no proxy configured; call with_http_proxy() first".into(),
457                ))
458            }
459        };
460
461        let tls_connector = tls::build_tls_connector(
462            &self.trusted_certs_der,
463            &self.alpn,
464            self.accept_invalid_certs,
465            self.use_webpki_roots,
466            self.key_log_path.clone(),
467            self.early_data,
468        )?;
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 = tls::build_tls_connector(
529            &self.trusted_certs_der,
530            &self.alpn,
531            self.accept_invalid_certs,
532            self.use_webpki_roots,
533            self.key_log_path.clone(),
534            self.early_data,
535        )?;
536
537        let socks_connector = Socks5Connector::new(proxy_uri, self.connect_timeout);
538        let https_connector = OxiHttpsConnector::new(socks_connector, tls_connector);
539
540        let mut builder = HyperClient::builder(TokioExecutor::new());
541        if let Some(n) = self.pool_max_idle_per_host {
542            builder.pool_max_idle_per_host(n);
543        }
544        if let Some(dur) = self.pool_idle_timeout {
545            builder.pool_idle_timeout(dur);
546        }
547        if let Some(ref h2) = self.http2_settings {
548            apply_http2_settings(&mut builder, h2);
549        }
550        let inner = builder.build(https_connector);
551        let mut default_headers = self.default_headers;
552        if let Some(agent) = &self.user_agent {
553            if let Ok(val) = HeaderValue::from_str(agent) {
554                default_headers.insert(http::header::USER_AGENT, val);
555            }
556        }
557        Ok(Client {
558            inner,
559            redirect_policy: self.redirect_policy,
560            retry_policy: self.retry_policy,
561            default_headers,
562            connect_timeout: self.connect_timeout,
563            read_timeout: self.read_timeout,
564            decompression: self.decompression,
565            middleware: self.middleware,
566            cookie_jar: self.cookie_jar.clone(),
567            #[cfg(feature = "tls")]
568            tls_rebuild: None,
569        })
570    }
571
572    // --- TLS-specific builder methods (feature-gated) -----------------------
573
574    /// Enable TLS with the Mozilla CA bundle (webpki-roots) as the trust store.
575    ///
576    /// Required to be called (or `with_webpki_roots` / `with_trusted_cert_der`)
577    /// before `build_https()` returns a usable client for real HTTPS endpoints.
578    #[cfg(feature = "tls")]
579    pub fn with_tls(mut self) -> Self {
580        self.use_webpki_roots = true;
581        self
582    }
583
584    /// Trust the Mozilla CA bundle (webpki-roots).
585    #[cfg(feature = "tls")]
586    pub fn with_webpki_roots(mut self) -> Self {
587        self.use_webpki_roots = true;
588        self
589    }
590
591    /// Trust an additional DER-encoded CA certificate.
592    ///
593    /// Can be called multiple times to add several trusted roots.
594    #[cfg(feature = "tls")]
595    pub fn with_trusted_cert_der(mut self, der: Vec<u8>) -> Self {
596        self.trusted_certs_der.push(der);
597        self
598    }
599
600    /// Set ALPN protocols to advertise (e.g. `&["h2", "http/1.1"]`).
601    #[cfg(feature = "tls")]
602    pub fn with_alpn(mut self, protocols: &[&str]) -> Self {
603        self.alpn = protocols.iter().map(|s| s.to_string()).collect();
604        self
605    }
606
607    /// **DANGER**: Accept any server certificate, including self-signed ones.
608    ///
609    /// For testing only — disables all certificate verification.
610    #[cfg(feature = "tls")]
611    pub fn with_danger_accept_invalid_certs(mut self) -> Self {
612        self.accept_invalid_certs = true;
613        self
614    }
615
616    /// Write TLS session secrets to `path` in NSS key-log format (SSLKEYLOGFILE).
617    ///
618    /// The file is created/appended to on every TLS handshake. Use this for
619    /// decrypting captured HTTPS traffic in Wireshark or mitmproxy during
620    /// development. **Do not enable in production.**
621    #[cfg(feature = "tls")]
622    pub fn with_key_log_file(mut self, path: std::path::PathBuf) -> Self {
623        self.key_log_path = Some(path);
624        self
625    }
626
627    /// Enable TLS 1.3 0-RTT early data (HTTP fast-open) for subsequent requests.
628    ///
629    /// Only effective if a prior connection stored a session ticket and the server
630    /// indicated `max_early_data_size > 0`. Safe to call on any builder; ignored
631    /// if 0-RTT is not available.
632    ///
633    /// # Security
634    /// Early data is NOT protected against replay attacks — see RFC 8446 §8.
635    /// Only enable for idempotent requests (GET, HEAD, etc.).
636    #[cfg(feature = "tls")]
637    pub fn with_early_data(mut self) -> Self {
638        self.early_data = true;
639        self
640    }
641
642    // --- build() — plain HTTP -----------------------------------------------
643
644    /// Build a plain HTTP `Client` (no TLS).
645    pub fn build(self) -> Result<Client, OxiHttpError> {
646        let mut builder = HyperClient::builder(TokioExecutor::new());
647
648        if let Some(n) = self.pool_max_idle_per_host {
649            builder.pool_max_idle_per_host(n);
650        }
651        if let Some(dur) = self.pool_idle_timeout {
652            builder.pool_idle_timeout(dur);
653        }
654        if let Some(ref h2) = self.http2_settings {
655            apply_http2_settings(&mut builder, h2);
656        }
657
658        // Use an explicit HttpConnector so TCP options can be applied.
659        let mut http = HttpConnector::new();
660        http.enforce_http(false);
661        if let Some(dur) = self.connect_timeout {
662            http.set_connect_timeout(Some(dur));
663        }
664        if let Some(nodelay) = self.tcp_nodelay {
665            http.set_nodelay(nodelay);
666        }
667        if let Some(ka) = self.tcp_keepalive {
668            http.set_keepalive(Some(ka));
669        }
670
671        let inner = builder.build(http);
672
673        let mut default_headers = self.default_headers;
674        if let Some(agent) = &self.user_agent {
675            if let Ok(val) = HeaderValue::from_str(agent) {
676                default_headers.insert(http::header::USER_AGENT, val);
677            }
678        }
679
680        Ok(Client {
681            inner,
682            redirect_policy: self.redirect_policy,
683            retry_policy: self.retry_policy,
684            default_headers,
685            connect_timeout: self.connect_timeout,
686            read_timeout: self.read_timeout,
687            decompression: self.decompression,
688            middleware: self.middleware,
689            cookie_jar: self.cookie_jar.clone(),
690            #[cfg(feature = "tls")]
691            tls_rebuild: None,
692        })
693    }
694
695    // --- build_https() — TLS-capable client ---------------------------------
696
697    /// Build a TLS-capable `HttpsClient` (alias for `Client<OxiHttpsConnector<HttpConnector>>`).
698    ///
699    /// The resulting client handles both `http://` and `https://` URIs.
700    #[cfg(feature = "tls")]
701    pub fn build_https(self) -> Result<super::HttpsClient, OxiHttpError> {
702        let connector = tls::build_tls_connector(
703            &self.trusted_certs_der,
704            &self.alpn,
705            self.accept_invalid_certs,
706            self.use_webpki_roots,
707            self.key_log_path.clone(),
708            self.early_data,
709        )?;
710
711        let mut http = HttpConnector::new();
712        // Allow the inner connector to accept https:// URIs so that the
713        // OxiHttpsConnector can extract the host/port and then upgrade the TCP
714        // stream to TLS.  Without this flag, HttpConnector rejects any URI
715        // whose scheme is not "http".
716        http.enforce_http(false);
717        if let Some(dur) = self.connect_timeout {
718            http.set_connect_timeout(Some(dur));
719        }
720        if let Some(nodelay) = self.tcp_nodelay {
721            http.set_nodelay(nodelay);
722        }
723        if let Some(ka) = self.tcp_keepalive {
724            http.set_keepalive(Some(ka));
725        }
726        let https_connector = OxiHttpsConnector::new(http, connector);
727
728        let mut builder = HyperClient::builder(TokioExecutor::new());
729
730        if let Some(n) = self.pool_max_idle_per_host {
731            builder.pool_max_idle_per_host(n);
732        }
733        if let Some(dur) = self.pool_idle_timeout {
734            builder.pool_idle_timeout(dur);
735        }
736        if let Some(ref h2) = self.http2_settings {
737            apply_http2_settings(&mut builder, h2);
738        }
739
740        let inner = builder.build(https_connector);
741
742        let mut default_headers = self.default_headers;
743        if let Some(agent) = &self.user_agent {
744            if let Ok(val) = HeaderValue::from_str(agent) {
745                default_headers.insert(http::header::USER_AGENT, val);
746            }
747        }
748
749        let tls_rebuild = Arc::new(TlsRebuildConfig {
750            trusted_certs_der: self.trusted_certs_der.clone(),
751            alpn: self.alpn.clone(),
752            accept_invalid_certs: self.accept_invalid_certs,
753            use_webpki_roots: self.use_webpki_roots,
754            key_log_path: self.key_log_path.clone(),
755            early_data: self.early_data,
756            connect_timeout: self.connect_timeout,
757            tcp_nodelay: self.tcp_nodelay,
758            tcp_keepalive: self.tcp_keepalive,
759            http2_settings: self.http2_settings.clone(),
760            pool_max_idle_per_host: self.pool_max_idle_per_host,
761            pool_idle_timeout: self.pool_idle_timeout,
762        });
763
764        Ok(Client {
765            inner,
766            redirect_policy: self.redirect_policy,
767            retry_policy: self.retry_policy,
768            default_headers,
769            connect_timeout: self.connect_timeout,
770            read_timeout: self.read_timeout,
771            decompression: self.decompression,
772            middleware: self.middleware,
773            cookie_jar: self.cookie_jar.clone(),
774            tls_rebuild: Some(tls_rebuild),
775        })
776    }
777
778    // --- build_with_resolver / build_https_with_resolver ---------------------
779
780    /// Build a plain HTTP `Client` using a custom DNS resolver.
781    ///
782    /// [`ClientBuilder::with_resolver`] must be called before this.
783    pub fn build_with_resolver(self) -> Result<super::ResolverClient, OxiHttpError> {
784        let resolver = self.resolver.ok_or_else(|| {
785            OxiHttpError::Dns("with_resolver must be called before build_with_resolver".into())
786        })?;
787        let mut http = HttpConnector::new_with_resolver(BoxResolver(resolver));
788        http.enforce_http(false);
789        if let Some(dur) = self.connect_timeout {
790            http.set_connect_timeout(Some(dur));
791        }
792        if let Some(nodelay) = self.tcp_nodelay {
793            http.set_nodelay(nodelay);
794        }
795        if let Some(ka) = self.tcp_keepalive {
796            http.set_keepalive(Some(ka));
797        }
798
799        let mut builder = HyperClient::builder(TokioExecutor::new());
800        if let Some(n) = self.pool_max_idle_per_host {
801            builder.pool_max_idle_per_host(n);
802        }
803        if let Some(d) = self.pool_idle_timeout {
804            builder.pool_idle_timeout(d);
805        }
806        if let Some(ref h2) = self.http2_settings {
807            apply_http2_settings(&mut builder, h2);
808        }
809
810        let mut default_headers = self.default_headers;
811        if let Some(agent) = &self.user_agent {
812            if let Ok(val) = HeaderValue::from_str(agent) {
813                default_headers.insert(http::header::USER_AGENT, val);
814            }
815        }
816
817        Ok(Client {
818            inner: builder.build(http),
819            connect_timeout: self.connect_timeout,
820            read_timeout: self.read_timeout,
821            redirect_policy: self.redirect_policy,
822            retry_policy: self.retry_policy,
823            default_headers,
824            decompression: self.decompression,
825            middleware: self.middleware,
826            cookie_jar: self.cookie_jar,
827            #[cfg(feature = "tls")]
828            tls_rebuild: None,
829        })
830    }
831
832    /// Build a TLS-capable `Client` using a custom DNS resolver.
833    ///
834    /// [`ClientBuilder::with_resolver`] must be called before this.
835    /// The resulting client handles both `http://` and `https://` URIs.
836    #[cfg(feature = "tls")]
837    pub fn build_https_with_resolver(self) -> Result<super::ResolverHttpsClient, OxiHttpError> {
838        let resolver = self.resolver.ok_or_else(|| {
839            OxiHttpError::Dns(
840                "with_resolver must be called before build_https_with_resolver".into(),
841            )
842        })?;
843
844        let tls_connector = tls::build_tls_connector(
845            &self.trusted_certs_der,
846            &self.alpn,
847            self.accept_invalid_certs,
848            self.use_webpki_roots,
849            self.key_log_path.clone(),
850            self.early_data,
851        )?;
852
853        let mut http = HttpConnector::new_with_resolver(BoxResolver(resolver));
854        http.enforce_http(false);
855        if let Some(dur) = self.connect_timeout {
856            http.set_connect_timeout(Some(dur));
857        }
858        if let Some(nodelay) = self.tcp_nodelay {
859            http.set_nodelay(nodelay);
860        }
861        if let Some(ka) = self.tcp_keepalive {
862            http.set_keepalive(Some(ka));
863        }
864
865        let connector = crate::connector::OxiHttpsConnector::new(http, tls_connector);
866
867        let mut builder = HyperClient::builder(TokioExecutor::new());
868        if let Some(n) = self.pool_max_idle_per_host {
869            builder.pool_max_idle_per_host(n);
870        }
871        if let Some(d) = self.pool_idle_timeout {
872            builder.pool_idle_timeout(d);
873        }
874        if let Some(ref h2) = self.http2_settings {
875            apply_http2_settings(&mut builder, h2);
876        }
877
878        let mut default_headers = self.default_headers;
879        if let Some(agent) = &self.user_agent {
880            if let Ok(val) = HeaderValue::from_str(agent) {
881                default_headers.insert(http::header::USER_AGENT, val);
882            }
883        }
884
885        Ok(Client {
886            inner: builder.build(connector),
887            connect_timeout: self.connect_timeout,
888            read_timeout: self.read_timeout,
889            redirect_policy: self.redirect_policy,
890            retry_policy: self.retry_policy,
891            default_headers,
892            decompression: self.decompression,
893            middleware: self.middleware,
894            cookie_jar: self.cookie_jar,
895            #[cfg(feature = "tls")]
896            tls_rebuild: None,
897        })
898    }
899}
900
901impl Default for ClientBuilder {
902    fn default() -> Self {
903        Self::new()
904    }
905}
906
907impl std::fmt::Debug for ClientBuilder {
908    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
909        f.debug_struct("ClientBuilder")
910            .field("pool_max_idle_per_host", &self.pool_max_idle_per_host)
911            .field("pool_idle_timeout", &self.pool_idle_timeout)
912            .field("connect_timeout", &self.connect_timeout)
913            .field("read_timeout", &self.read_timeout)
914            .field("redirect_policy", &self.redirect_policy)
915            .field("retry_policy", &self.retry_policy)
916            .field("decompression", &self.decompression)
917            .field("user_agent", &self.user_agent)
918            .field("tcp_nodelay", &self.tcp_nodelay)
919            .field("tcp_keepalive", &self.tcp_keepalive)
920            .finish_non_exhaustive()
921    }
922}