Skip to main content

specter/transport/
h1_h2.rs

1//! Unified HTTP/1.1, HTTP/2, and HTTP/3 client.
2//!
3//! Uses:
4//! - h1.rs for HTTP/1.1 (minimal httparse-based implementation)
5//! - h2.rs for HTTP/2 (with full SETTINGS fingerprinting and connection pooling)
6//! - h3.rs for HTTP/3 (via quiche QUIC)
7//!
8//! Supports automatic HTTP/3 upgrade via Alt-Svc header caching.
9
10use base64::Engine;
11use bytes::Bytes;
12use http::{Method, Uri};
13use serde::Serialize;
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::Duration;
17use tokio::sync::RwLock;
18use tokio::time::timeout as tokio_timeout;
19use url::Url;
20
21use crate::cookie::CookieJar;
22use crate::error::{Error, Result};
23use crate::fingerprint::{http2::Http2Settings, FingerprintProfile};
24use crate::headers::Headers;
25use crate::pool::alt_svc::AltSvcCache;
26use crate::pool::multiplexer::{ConnectionPool, PoolKey};
27use crate::request::{Body, IntoUrl, RedirectPolicy, Request};
28use crate::response::Response;
29use crate::timeouts::Timeouts;
30use crate::transport::connector::{BoringConnector, MaybeHttpsStream};
31use crate::transport::h1::H1Connection;
32use crate::transport::h2::{H2Connection, H2PooledConnection, PseudoHeaderOrder};
33use crate::transport::h3::H3Client;
34use crate::version::HttpVersion;
35
36/// Unified HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support.
37///
38/// Provides automatic protocol selection based on ALPN negotiation and
39/// Alt-Svc header caching for HTTP/3 upgrades.
40///
41/// HTTP/2 connections are pooled and multiplexed - multiple concurrent requests
42/// to the same host:port share a single TCP connection.
43/// HTTP/1.1 connections are also pooled for reuse via keep-alive.
44#[derive(Clone)]
45pub struct Client {
46    connector: BoringConnector,
47    /// Connector with TLS verification disabled (for localhost)
48    insecure_connector: BoringConnector,
49    h3_client: H3Client,
50    alt_svc_cache: Arc<AltSvcCache>,
51    /// HTTP/2 connection pool for multiplexing
52    h2_pool: Arc<RwLock<HashMap<PoolKey, H2PooledConnection>>>,
53    /// HTTP/1.1 connection pool for reuse
54    h1_pool: Arc<ConnectionPool>,
55    http2_settings: Http2Settings,
56    pseudo_order: PseudoHeaderOrder,
57    default_version: HttpVersion,
58    /// Timeout configuration
59    timeouts: Timeouts,
60    /// Whether to opportunistically try HTTP/3 when Alt-Svc indicates support
61    h3_upgrade_enabled: bool,
62    /// Force HTTP/2 prior knowledge (H2C) for cleartext connections
63    http2_prior_knowledge: bool,
64    /// Skip TLS verification for all connections
65    danger_accept_invalid_certs: bool,
66    /// Skip TLS verification for localhost connections only
67    localhost_allows_invalid_certs: bool,
68    /// Default headers applied to every request
69    default_headers: Headers,
70    /// Redirect policy
71    redirect_policy: RedirectPolicy,
72    /// Optional cookie store
73    cookie_store: Option<Arc<RwLock<CookieJar>>>,
74}
75
76/// Builder for HTTP requests.
77pub struct RequestBuilder<'a> {
78    client: &'a Client,
79    url: Option<Url>,
80    method: Method,
81    headers: Headers,
82    body: Body,
83    version: Option<HttpVersion>,
84    timeout: Option<Duration>,
85    error: Option<Error>,
86}
87
88/// Builder for creating HTTP clients.
89pub struct ClientBuilder {
90    fingerprint: FingerprintProfile,
91    http2_settings: Option<Http2Settings>,
92    pseudo_order: PseudoHeaderOrder,
93    timeouts: Timeouts,
94    prefer_http2: bool,
95    h3_upgrade_enabled: bool,
96    http2_prior_knowledge: bool,
97    root_certs: Vec<Vec<u8>>,
98    /// Load root certificates from the OS certificate store at runtime
99    use_platform_roots: bool,
100    /// Skip TLS certificate verification (DANGEROUS - for testing only)
101    danger_accept_invalid_certs: bool,
102    /// Automatically skip TLS verification for localhost connections
103    localhost_allows_invalid_certs: bool,
104    /// Default headers applied to every request
105    default_headers: Headers,
106    /// Redirect policy
107    redirect_policy: RedirectPolicy,
108    /// Optional cookie store
109    cookie_store: Option<Arc<RwLock<CookieJar>>>,
110}
111
112impl Client {
113    /// Create a new client with default settings.
114    pub fn new() -> Result<Self> {
115        ClientBuilder::new().build()
116    }
117
118    /// Create a new client builder.
119    pub fn builder() -> ClientBuilder {
120        ClientBuilder::new()
121    }
122
123    /// Create a GET request builder.
124    pub fn get(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
125        RequestBuilder::new(self, Method::GET, url)
126    }
127
128    /// Create a POST request builder.
129    pub fn post(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
130        RequestBuilder::new(self, Method::POST, url)
131    }
132
133    /// Create a PUT request builder.
134    pub fn put(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
135        RequestBuilder::new(self, Method::PUT, url)
136    }
137
138    /// Create a DELETE request builder.
139    pub fn delete(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
140        RequestBuilder::new(self, Method::DELETE, url)
141    }
142
143    /// Create a HEAD request builder.
144    pub fn head(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
145        RequestBuilder::new(self, Method::HEAD, url)
146    }
147
148    /// Create a PATCH request builder.
149    pub fn patch(&self, url: impl IntoUrl) -> RequestBuilder<'_> {
150        RequestBuilder::new(self, Method::PATCH, url)
151    }
152
153    /// Create a custom method request builder.
154    pub fn request(&self, method: Method, url: impl IntoUrl) -> RequestBuilder<'_> {
155        RequestBuilder::new(self, method, url)
156    }
157
158    /// Get the Alt-Svc cache for manual inspection or manipulation.
159    pub fn alt_svc_cache(&self) -> &Arc<AltSvcCache> {
160        &self.alt_svc_cache
161    }
162
163    /// Check if a host is localhost (localhost, 127.0.0.1, ::1)
164    fn is_localhost(host: &str) -> bool {
165        host == "localhost" || host == "127.0.0.1" || host == "::1"
166    }
167
168    /// Get the appropriate connector for a URI (uses insecure connector for localhost if enabled)
169    fn connector_for_uri(&self, uri: &Uri) -> &BoringConnector {
170        // Always use insecure connector if danger_accept_invalid_certs is globally enabled
171        if self.danger_accept_invalid_certs {
172            return &self.insecure_connector;
173        }
174
175        // Use insecure connector for localhost if localhost_allows_invalid_certs is enabled
176        if self.localhost_allows_invalid_certs {
177            if let Some(host) = uri.host() {
178                if Self::is_localhost(host) {
179                    return &self.insecure_connector;
180                }
181            }
182        }
183
184        &self.connector
185    }
186}
187
188impl<'a> RequestBuilder<'a> {
189    fn new(client: &'a Client, method: Method, url: impl IntoUrl) -> Self {
190        let mut error = None;
191        let url = match url.into_url() {
192            Ok(url) => Some(url),
193            Err(err) => {
194                error = Some(err);
195                None
196            }
197        };
198
199        Self {
200            client,
201            url,
202            method,
203            headers: client.default_headers.clone(),
204            body: Body::Empty,
205            version: None,
206            timeout: None,
207            error,
208        }
209    }
210
211    fn set_error(&mut self, error: Error) {
212        if self.error.is_none() {
213            self.error = Some(error);
214        }
215    }
216
217    fn ensure_content_type(&mut self, value: &str) {
218        if !self.headers.contains("content-type") {
219            self.headers.insert("Content-Type", value.to_string());
220        }
221    }
222
223    /// Add a header to the request.
224    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
225        self.headers.insert(key, value);
226        self
227    }
228
229    /// Append a header without replacing existing values.
230    pub fn header_append(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
231        self.headers.append(key, value);
232        self
233    }
234
235    /// Set all headers (replaces existing headers).
236    pub fn headers(mut self, headers: impl Into<Headers>) -> Self {
237        self.headers = headers.into();
238        self
239    }
240
241    /// Set the request body.
242    pub fn body(mut self, body: impl Into<Body>) -> Self {
243        self.body = body.into();
244        self
245    }
246
247    /// Add URL query parameters.
248    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
249        if self.error.is_some() {
250            return self;
251        }
252
253        let url = match self.url.as_mut() {
254            Some(url) => url,
255            None => return self,
256        };
257
258        match serde_urlencoded::to_string(query) {
259            Ok(encoded) => {
260                if !encoded.is_empty() {
261                    let merged = match url.query() {
262                        Some(existing) if !existing.is_empty() => {
263                            format!("{}&{}", existing, encoded)
264                        }
265                        _ => encoded,
266                    };
267                    url.set_query(Some(&merged));
268                }
269            }
270            Err(err) => self.set_error(err.into()),
271        }
272
273        self
274    }
275
276    /// Set a JSON body.
277    pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
278        if self.error.is_some() {
279            return self;
280        }
281
282        match serde_json::to_vec(json) {
283            Ok(bytes) => {
284                self.body = Body::Json(bytes);
285                self.ensure_content_type("application/json");
286            }
287            Err(err) => self.set_error(err.into()),
288        }
289
290        self
291    }
292
293    /// Set a form-encoded body.
294    pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> Self {
295        if self.error.is_some() {
296            return self;
297        }
298
299        match serde_urlencoded::to_string(form) {
300            Ok(encoded) => {
301                self.body = Body::Form(encoded);
302                self.ensure_content_type("application/x-www-form-urlencoded");
303            }
304            Err(err) => self.set_error(err.into()),
305        }
306
307        self
308    }
309
310    /// Set a bearer token for Authorization header.
311    pub fn bearer_auth(mut self, token: impl AsRef<str>) -> Self {
312        self.headers
313            .insert("Authorization", format!("Bearer {}", token.as_ref()));
314        self
315    }
316
317    /// Set basic auth for Authorization header.
318    pub fn basic_auth<P: AsRef<str>>(
319        mut self,
320        username: impl AsRef<str>,
321        password: Option<P>,
322    ) -> Self {
323        let creds = match password {
324            Some(p) => format!("{}:{}", username.as_ref(), p.as_ref()),
325            None => format!("{}:", username.as_ref()),
326        };
327        let encoded = base64::engine::general_purpose::STANDARD.encode(creds.as_bytes());
328        self.headers
329            .insert("Authorization", format!("Basic {}", encoded));
330        self
331    }
332
333    /// Set per-request total timeout.
334    pub fn timeout(mut self, timeout: Duration) -> Self {
335        self.timeout = Some(timeout);
336        self
337    }
338
339    /// Set the HTTP version preference.
340    pub fn version(mut self, version: HttpVersion) -> Self {
341        self.version = Some(version);
342        self
343    }
344
345    /// Build a request without sending it.
346    pub fn build(self) -> Result<Request> {
347        if let Some(error) = self.error {
348            return Err(error);
349        }
350
351        let url = self.url.ok_or_else(|| Error::missing("url"))?;
352
353        Ok(Request {
354            method: self.method,
355            url,
356            headers: self.headers,
357            body: self.body,
358            version: self.version,
359            timeout: self.timeout,
360        })
361    }
362
363    /// Send the request and return the response.
364    pub async fn send(self) -> Result<Response> {
365        let client = self.client.clone();
366        let request = self.build()?;
367        client.execute(request).await
368    }
369
370    /// Send the request and return the response with streaming body.
371    /// Returns (Response, Receiver for body chunks).
372    /// The response body is empty - chunks arrive via the receiver.
373    pub async fn send_streaming(
374        self,
375    ) -> Result<(
376        Response,
377        tokio::sync::mpsc::Receiver<std::result::Result<Bytes, crate::transport::h2::H2Error>>,
378    )> {
379        let client = self.client.clone();
380        let request = self.build()?;
381        let mut timeouts = client.timeouts.clone();
382        if let Some(total) = request.timeout {
383            timeouts.total = Some(total);
384        }
385        let mut headers = request.headers.clone();
386
387        if let Some(jar) = &client.cookie_store {
388            if !headers.contains("cookie") {
389                if let Some(cookie_header) =
390                    jar.read().await.build_cookie_header(request.url.as_str())
391                {
392                    headers.insert("Cookie", cookie_header);
393                }
394            }
395        }
396
397        let version = request.version.unwrap_or(client.default_version);
398
399        // Only HTTP/2 supports streaming currently
400        if !matches!(version, HttpVersion::Http2 | HttpVersion::Auto) {
401            return Err(Error::HttpProtocol(
402                "Streaming only supported for HTTP/2".into(),
403            ));
404        }
405
406        // Parse URI
407        let uri: Uri = request
408            .url
409            .as_str()
410            .parse()
411            .map_err(|e| Error::HttpProtocol(format!("Invalid URI: {}", e)))?;
412
413        // For HTTP/2 streaming, we need direct connection access (no pooling for streaming)
414        // Apply connect timeout to TCP + TLS handshake
415        let connector = client.connector_for_uri(&uri);
416        let connect_fut = connector.connect(&uri);
417        let stream = if let Some(connect_timeout) = timeouts.connect {
418            tokio_timeout(connect_timeout, connect_fut)
419                .await
420                .map_err(|_| Error::ConnectTimeout(connect_timeout))??
421        } else {
422            connect_fut.await?
423        };
424
425        // Ensure ALPN negotiated h2
426        let alpn = stream.alpn_protocol();
427        if !alpn.is_h2() {
428            return Err(Error::HttpProtocol(format!(
429                "Expected h2 ALPN, got {:?}",
430                alpn
431            )));
432        }
433
434        // Create H2 connection (part of connect phase)
435        let h2_connect_fut =
436            H2Connection::connect(stream, client.http2_settings.clone(), client.pseudo_order);
437        let mut h2_conn = if let Some(connect_timeout) = timeouts.connect {
438            tokio_timeout(connect_timeout, h2_connect_fut)
439                .await
440                .map_err(|_| Error::ConnectTimeout(connect_timeout))??
441        } else {
442            h2_connect_fut.await?
443        };
444
445        // Build HTTP request
446        let mut path = request.url.path().to_string();
447        if path.is_empty() {
448            path = "/".to_string();
449        }
450        if let Some(query) = request.url.query() {
451            path.push('?');
452            path.push_str(query);
453        }
454
455        let host = request.url.host_str().unwrap_or("localhost");
456        let authority = if let Some(port) = request.url.port_or_known_default() {
457            if port == 443 {
458                host.to_string()
459            } else {
460                format!("{}:{}", host, port)
461            }
462        } else {
463            host.to_string()
464        };
465
466        // Build URI with scheme and authority for HTTP/2
467        let full_uri = format!("https://{}{}", authority, path);
468        let mut request_builder = http::Request::builder()
469            .method(request.method.clone())
470            .uri(&full_uri);
471
472        // Add custom headers (pseudo-headers are derived from method/uri)
473        for (key, value) in headers.iter() {
474            request_builder = request_builder.header(key, value);
475        }
476
477        let body = request.body.clone().into_bytes()?;
478        let http_request = request_builder
479            .body(body)
480            .map_err(|e| Error::HttpProtocol(format!("Failed to build request: {}", e)))?;
481
482        // Send streaming request with TTFB timeout
483        let send_fut = h2_conn.send_request_streaming(http_request);
484        let (response, rx) = if let Some(ttfb_timeout) = timeouts.ttfb {
485            tokio_timeout(ttfb_timeout, send_fut)
486                .await
487                .map_err(|_| Error::TtfbTimeout(ttfb_timeout))??
488        } else {
489            send_fut.await?
490        };
491
492        // Spawn task to read streaming frames
493        tokio::spawn(async move {
494            loop {
495                match h2_conn.read_streaming_frames().await {
496                    Ok(true) => continue,
497                    Ok(false) => break,
498                    Err(e) => {
499                        tracing::debug!("Streaming read error: {}", e);
500                        break;
501                    }
502                }
503            }
504        });
505
506        // Convert http::Response to our Response type
507        let status = response.status().as_u16();
508        let headers = response
509            .headers()
510            .iter()
511            .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
512            .collect::<Vec<(String, String)>>();
513
514        let our_response = crate::response::Response::new(
515            status,
516            Headers::from(headers),
517            Bytes::new(), // Body comes through rx
518            "HTTP/2".to_string(),
519        );
520
521        let request_url = request.url.clone();
522        let our_response = our_response.with_url(request_url.clone());
523
524        if let Some(jar) = &client.cookie_store {
525            jar.write()
526                .await
527                .store_from_headers(our_response.headers(), request_url.as_str());
528        }
529
530        Ok((our_response, rx))
531    }
532}
533
534impl Client {
535    /// Execute a built request with client policy (redirects, cookies, etc.).
536    pub async fn execute(&self, mut request: Request) -> Result<Response> {
537        let policy = self.redirect_policy.clone();
538        let mut redirects = 0u32;
539
540        loop {
541            let mut headers = request.headers.clone();
542            let cookie_injected = self.apply_cookie_header(&request, &mut headers).await;
543            request.headers = headers;
544
545            let mut timeouts = self.timeouts.clone();
546            if let Some(total) = request.timeout {
547                timeouts.total = Some(total);
548            }
549
550            let response = self.execute_once(&request, &timeouts).await?;
551
552            self.store_cookies(&response, &request.url).await;
553
554            if matches!(policy, RedirectPolicy::None) || !response.is_redirect() {
555                return Ok(response);
556            }
557
558            let location = match response.redirect_url() {
559                Some(value) => value,
560                None => return Ok(response),
561            };
562
563            if let RedirectPolicy::Limited(limit) = policy {
564                if redirects >= limit {
565                    return Err(Error::RedirectLimit { count: limit });
566                }
567            }
568
569            let next_url = request.url.join(location).map_err(Error::from)?;
570            let mut next_request = self.redirect_request(&request, &response, next_url);
571
572            if cookie_injected {
573                next_request.headers.remove("cookie");
574            }
575
576            request = next_request;
577            redirects += 1;
578        }
579    }
580
581    async fn execute_once(&self, request: &Request, timeouts: &Timeouts) -> Result<Response> {
582        let version = request.version.unwrap_or(self.default_version);
583
584        // HTTP/3 only - go directly to H3
585        if matches!(version, HttpVersion::Http3Only) {
586            return self
587                .send_h3_for_url(request, request.url.clone(), timeouts)
588                .await;
589        }
590
591        // HTTP/3 preferred - try H3 first, fall back to H1/H2
592        if matches!(version, HttpVersion::Http3) {
593            match self
594                .send_h3_for_url(request, request.url.clone(), timeouts)
595                .await
596            {
597                Ok(response) => return Ok(response),
598                Err(e) => {
599                    tracing::debug!("HTTP/3 failed, falling back to HTTP/1.1 or HTTP/2: {}", e);
600                    // Fall through to H1/H2
601                }
602            }
603        }
604
605        // Auto mode - check Alt-Svc cache for HTTP/3 upgrade opportunity
606        if matches!(version, HttpVersion::Auto) && self.h3_upgrade_enabled {
607            let origin = Self::origin_for_url(&request.url);
608            if let Some(alt_svc) = self.alt_svc_cache.get_h3_alternative(&origin).await {
609                tracing::debug!(
610                    "Alt-Svc indicates HTTP/3 support for {}, attempting upgrade",
611                    origin
612                );
613
614                let mut h3_url = request.url.clone();
615                let _ = h3_url.set_scheme("https");
616                if let Some(ref host) = alt_svc.host {
617                    h3_url
618                        .set_host(Some(host))
619                        .map_err(|_| Error::HttpProtocol("Invalid Alt-Svc host".into()))?;
620                }
621                let _ = h3_url.set_port(Some(alt_svc.port));
622
623                match self
624                    .send_h3_for_url(request, h3_url.clone(), timeouts)
625                    .await
626                {
627                    Ok(response) => return Ok(response.with_url(h3_url)),
628                    Err(e) => {
629                        tracing::debug!("HTTP/3 upgrade failed, using HTTP/1.1 or HTTP/2: {}", e);
630                        // Fall through to H1/H2
631                    }
632                }
633            }
634        }
635
636        // HTTP/1.1 or HTTP/2 via TCP+TLS
637        self.send_h1_h2(request, version, timeouts).await
638    }
639
640    async fn send_h3_for_url(
641        &self,
642        request: &Request,
643        url: Url,
644        timeouts: &Timeouts,
645    ) -> Result<Response> {
646        let body = if request.body.is_empty() {
647            None
648        } else {
649            Some(request.body.clone().into_bytes()?.to_vec())
650        };
651
652        let fut = self.h3_client.send_request(
653            url.as_str(),
654            request.method.as_str(),
655            request.headers.to_vec(),
656            body,
657        );
658
659        // Apply total timeout for HTTP/3 (includes connect + request + response)
660        let response = if let Some(total_timeout) = timeouts.total {
661            tokio_timeout(total_timeout, fut)
662                .await
663                .map_err(|_| Error::TotalTimeout(total_timeout))??
664        } else {
665            fut.await?
666        };
667
668        Ok(response.with_url(url))
669    }
670
671    async fn send_h1_h2(
672        &self,
673        request: &Request,
674        version: HttpVersion,
675        timeouts: &Timeouts,
676    ) -> Result<Response> {
677        // Save the original URL for effective_url tracking
678        let request_url = request.url.clone();
679
680        // Parse URI
681        let uri: Uri = request
682            .url
683            .as_str()
684            .parse()
685            .map_err(|e| Error::HttpProtocol(format!("Invalid URI: {}", e)))?;
686
687        // Determine if we should use HTTP/2
688        let prefer_http2 = match version {
689            HttpVersion::Http1_1 => false,
690            HttpVersion::Http2 => true,
691            HttpVersion::Http3 | HttpVersion::Http3Only => {
692                return Err(Error::HttpProtocol("HTTP/3 should use send_h3".into()));
693            }
694            HttpVersion::Auto => matches!(self.default_version, HttpVersion::Http2),
695        };
696
697        // Extract values needed after potential moves
698        let h3_upgrade_enabled = self.h3_upgrade_enabled;
699        let alt_svc_cache = self.alt_svc_cache.clone();
700        let origin = Self::origin_for_url(&request.url);
701
702        let headers_vec = request.headers.to_vec();
703        let body_bytes = if request.body.is_empty() {
704            None
705        } else {
706            Some(request.body.clone().into_bytes()?)
707        };
708
709        // For HTTP/2, try to use pooled connection first
710        if prefer_http2 {
711            let pool_key = Self::make_pool_key(&uri);
712
713            // Check for existing pooled connection
714            let pooled = {
715                let pool = self.h2_pool.read().await;
716                pool.get(&pool_key).cloned()
717            };
718
719            if let Some(conn) = pooled {
720                // Try to use pooled connection
721                let result = conn
722                    .send_request(
723                        request.method.clone(),
724                        &uri,
725                        headers_vec.clone(),
726                        body_bytes.clone(),
727                    )
728                    .await;
729
730                match result {
731                    Ok(response) => {
732                        // Parse Alt-Svc header for HTTP/3 discovery
733                        if h3_upgrade_enabled {
734                            if let Some(alt_svc) = response.get_header("alt-svc") {
735                                alt_svc_cache.parse_and_store(&origin, alt_svc).await;
736                            }
737                        }
738                        return Ok(response.with_url(request_url));
739                    }
740                    Err(e) => {
741                        // Connection failed - remove from pool and create new one
742                        tracing::debug!("Pooled HTTP/2 connection failed, creating new: {}", e);
743                        let mut pool = self.h2_pool.write().await;
744                        pool.remove(&pool_key);
745                    }
746                }
747            }
748
749            // No pooled connection or it failed - create new one
750            // Apply connect timeout
751            let connector = self.connector_for_uri(&uri);
752            let connect_fut = connector.connect(&uri);
753            let stream = if let Some(connect_timeout) = timeouts.connect {
754                tokio_timeout(connect_timeout, connect_fut)
755                    .await
756                    .map_err(|_| Error::ConnectTimeout(connect_timeout))??
757            } else {
758                connect_fut.await?
759            };
760
761            // Verify ALPN negotiated h2
762            let use_http2 = if self.http2_prior_knowledge && !stream.alpn_protocol().is_h2() {
763                // For Prior Knowledge, we use H2 if strictly requested, even if no ALPN (e.g. cleartext)
764                true
765            } else if let MaybeHttpsStream::Https(ref ssl_stream) = stream {
766                ssl_stream.ssl().selected_alpn_protocol() == Some(b"h2")
767            } else {
768                false
769            };
770
771            if use_http2 {
772                // Create HTTP/2 connection and pool it
773                let h2_conn =
774                    H2Connection::connect(stream, self.http2_settings.clone(), self.pseudo_order)
775                        .await?;
776                let pooled_conn = H2PooledConnection::new(h2_conn);
777
778                // Store in pool
779                {
780                    let mut pool = self.h2_pool.write().await;
781                    pool.insert(pool_key, pooled_conn.clone());
782                }
783
784                // Send request with TTFB timeout
785                let fut = pooled_conn.send_request(
786                    request.method.clone(),
787                    &uri,
788                    headers_vec.clone(),
789                    body_bytes.clone(),
790                );
791
792                let response = if let Some(ttfb_timeout) = timeouts.ttfb {
793                    tokio_timeout(ttfb_timeout, fut)
794                        .await
795                        .map_err(|_| Error::TtfbTimeout(ttfb_timeout))?
796                } else {
797                    fut.await
798                }?;
799
800                // Parse Alt-Svc header for HTTP/3 discovery
801                if h3_upgrade_enabled {
802                    if let Some(alt_svc) = response.get_header("alt-svc") {
803                        alt_svc_cache.parse_and_store(&origin, alt_svc).await;
804                    }
805                }
806
807                return Ok(response.with_url(request_url));
808            }
809            // Fall through to HTTP/1.1 if h2 not negotiated
810        }
811
812        // HTTP/1.1 path (with connection pooling)
813        let pool_key = Self::make_pool_key(&uri);
814
815        // Try to get a pooled connection first
816        let mut stream_opt = self.h1_pool.get_h1(&pool_key).await;
817        let mut used_pooled = stream_opt.is_some();
818
819        // If no pooled connection, create a new one
820        let mut stream = if let Some(pooled_stream) = stream_opt.take() {
821            tracing::debug!("H1: Reusing pooled connection for {:?}", pool_key);
822            pooled_stream
823        } else {
824            tracing::debug!("H1: Creating new connection for {:?}", pool_key);
825            // Apply connect timeout
826            let connector = self.connector_for_uri(&uri);
827            let connect_fut = connector.connect(&uri);
828            if let Some(connect_timeout) = timeouts.connect {
829                tokio_timeout(connect_timeout, connect_fut)
830                    .await
831                    .map_err(|_| Error::ConnectTimeout(connect_timeout))??
832            } else {
833                connect_fut.await?
834            }
835        };
836
837        // Check if server negotiated HTTP/2 via ALPN - if so, we must use HTTP/2
838        // even though we preferred HTTP/1.1 (server choice takes precedence)
839        let server_wants_h2 = if let MaybeHttpsStream::Https(ref ssl_stream) = stream {
840            ssl_stream.ssl().selected_alpn_protocol() == Some(b"h2")
841        } else {
842            false
843        };
844
845        let response = if server_wants_h2 {
846            // Server negotiated HTTP/2 - we must speak HTTP/2 or they'll close connection
847            tracing::debug!("Server selected h2 via ALPN, upgrading to HTTP/2");
848
849            let h2_conn =
850                H2Connection::connect(stream, self.http2_settings.clone(), self.pseudo_order)
851                    .await?;
852            let pooled_conn = H2PooledConnection::new(h2_conn);
853
854            // Store in pool for reuse
855            {
856                let mut pool = self.h2_pool.write().await;
857                pool.insert(pool_key, pooled_conn.clone());
858            }
859
860            // Send request with TTFB timeout
861            let fut = pooled_conn.send_request(
862                request.method.clone(),
863                &uri,
864                headers_vec.clone(),
865                body_bytes.clone(),
866            );
867
868            if let Some(ttfb_timeout) = timeouts.ttfb {
869                tokio_timeout(ttfb_timeout, fut)
870                    .await
871                    .map_err(|_| Error::TtfbTimeout(ttfb_timeout))?
872            } else {
873                fut.await
874            }?
875        } else {
876            // HTTP/1.1 - use the stream we already connected (or got from pool)
877
878            // Send request - retry with new connection if pooled connection fails
879            let result = loop {
880                let stream_for_request = stream;
881                let fut = Self::do_send_http1(
882                    stream_for_request,
883                    request.method.clone(),
884                    &uri,
885                    headers_vec.clone(),
886                    body_bytes.clone(),
887                );
888
889                // Apply TTFB timeout for HTTP/1.1 request
890                let request_result = if let Some(ttfb_timeout) = timeouts.ttfb {
891                    tokio_timeout(ttfb_timeout, fut)
892                        .await
893                        .map_err(|_| Error::TtfbTimeout(ttfb_timeout))?
894                } else {
895                    fut.await
896                };
897
898                match request_result {
899                    Ok((resp, returned_stream)) => {
900                        // Success - return stream to pool for reuse
901                        self.h1_pool.put_h1(pool_key.clone(), returned_stream).await;
902                        break Ok(resp);
903                    }
904                    Err(e) => {
905                        // Check if this was a pooled connection that failed
906                        if used_pooled {
907                            tracing::debug!(
908                                "H1: Pooled connection failed for {:?}, creating new: {}",
909                                pool_key,
910                                e
911                            );
912                            // Try again with a fresh connection (with connect timeout)
913                            let connector = self.connector_for_uri(&uri);
914                            let connect_fut = connector.connect(&uri);
915                            stream = if let Some(connect_timeout) = timeouts.connect {
916                                tokio_timeout(connect_timeout, connect_fut)
917                                    .await
918                                    .map_err(|_| Error::ConnectTimeout(connect_timeout))??
919                            } else {
920                                connect_fut.await?
921                            };
922                            used_pooled = false; // Mark that we're no longer using a pooled connection
923                            continue;
924                        } else {
925                            // Fresh connection also failed - return error
926                            tracing::debug!(
927                                "H1: Request failed for {:?}, discarding connection: {}",
928                                pool_key,
929                                e
930                            );
931                            break Err(e);
932                        }
933                    }
934                }
935            };
936
937            result?
938        };
939
940        // Parse Alt-Svc header for HTTP/3 discovery
941        if h3_upgrade_enabled {
942            if let Some(alt_svc) = response.get_header("alt-svc") {
943                alt_svc_cache.parse_and_store(&origin, alt_svc).await;
944            }
945        }
946
947        Ok(response.with_url(request_url))
948    }
949
950    fn redirect_request(&self, request: &Request, response: &Response, next_url: Url) -> Request {
951        let status = response.status().as_u16();
952        let mut method = request.method.clone();
953        let mut body = request.body.clone();
954        let mut headers = request.headers.clone();
955
956        let should_switch = status == 303
957            || ((status == 301 || status == 302) && !matches!(method, Method::GET | Method::HEAD));
958
959        if should_switch {
960            method = Method::GET;
961            body = Body::Empty;
962            headers.remove("content-length");
963            headers.remove("content-type");
964        }
965
966        if Self::is_cross_origin(&request.url, &next_url) {
967            headers.remove("authorization");
968        }
969
970        Request {
971            method,
972            url: next_url,
973            headers,
974            body,
975            version: request.version,
976            timeout: request.timeout,
977        }
978    }
979
980    async fn apply_cookie_header(&self, request: &Request, headers: &mut Headers) -> bool {
981        if let Some(jar) = &self.cookie_store {
982            if !headers.contains("cookie") {
983                if let Some(cookie_header) =
984                    jar.read().await.build_cookie_header(request.url.as_str())
985                {
986                    headers.insert("Cookie", cookie_header);
987                    return true;
988                }
989            }
990        }
991        false
992    }
993
994    async fn store_cookies(&self, response: &Response, url: &Url) {
995        if let Some(jar) = &self.cookie_store {
996            jar.write()
997                .await
998                .store_from_headers(response.headers(), url.as_str());
999        }
1000    }
1001
1002    /// Create a pool key from a URI.
1003    fn make_pool_key(uri: &Uri) -> PoolKey {
1004        let host = uri.host().unwrap_or("localhost").to_string();
1005        let is_https = uri.scheme_str() == Some("https");
1006        let port = uri.port_u16().unwrap_or(if is_https { 443 } else { 80 });
1007        PoolKey::new(host, port, is_https)
1008    }
1009
1010    async fn do_send_http1(
1011        stream: MaybeHttpsStream,
1012        method: Method,
1013        uri: &Uri,
1014        headers: Vec<(String, String)>,
1015        body: Option<Bytes>,
1016    ) -> Result<(Response, MaybeHttpsStream)> {
1017        let mut conn = H1Connection::new(stream);
1018        let response = conn.send_request(method, uri, headers, body).await?;
1019        let stream = conn.into_inner();
1020        Ok((response, stream))
1021    }
1022
1023    /// Extract origin (scheme://host:port) from URL.
1024    fn origin_for_url(url: &Url) -> String {
1025        let scheme = url.scheme();
1026        let host = url.host_str().unwrap_or("localhost");
1027        let port = url
1028            .port_or_known_default()
1029            .unwrap_or(if scheme == "https" { 443 } else { 80 });
1030
1031        if (scheme == "https" && port == 443) || (scheme == "http" && port == 80) {
1032            format!("{}://{}", scheme, host)
1033        } else {
1034            format!("{}://{}:{}", scheme, host, port)
1035        }
1036    }
1037
1038    fn is_cross_origin(a: &Url, b: &Url) -> bool {
1039        a.scheme() != b.scheme()
1040            || a.host_str() != b.host_str()
1041            || a.port_or_known_default() != b.port_or_known_default()
1042    }
1043}
1044
1045impl ClientBuilder {
1046    /// Create a new client builder with default settings.
1047    ///
1048    /// By default, no timeouts are set. Use `timeouts()`, `api_timeouts()`,
1049    /// or `streaming_timeouts()` to configure timeouts.
1050    ///
1051    /// Localhost connections automatically skip TLS certificate verification
1052    /// by default, making local development easier. Use `localhost_allows_invalid_certs(false)`
1053    /// to disable this behavior.
1054    pub fn new() -> Self {
1055        Self {
1056            fingerprint: FingerprintProfile::default(),
1057            http2_settings: None,
1058            pseudo_order: PseudoHeaderOrder::Chrome,
1059            timeouts: Timeouts::default(),
1060            prefer_http2: true, // HTTP/2 preferred by default (falls back to HTTP/1.1 if not supported)
1061            h3_upgrade_enabled: true, // Enable by default
1062            http2_prior_knowledge: false,
1063            root_certs: Vec::new(),
1064            use_platform_roots: false,
1065            danger_accept_invalid_certs: false,
1066            localhost_allows_invalid_certs: true, // Enable by default for easier local dev
1067            default_headers: Headers::new(),
1068            redirect_policy: RedirectPolicy::None,
1069            cookie_store: None,
1070        }
1071    }
1072
1073    /// Set the fingerprint profile.
1074    pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
1075        self.fingerprint = fingerprint;
1076        self
1077    }
1078
1079    /// Set HTTP/2 settings for fingerprinting.
1080    pub fn http2_settings(mut self, settings: Http2Settings) -> Self {
1081        self.http2_settings = Some(settings);
1082        self
1083    }
1084
1085    /// Set pseudo-header ordering for HTTP/2 fingerprinting.
1086    pub fn pseudo_order(mut self, order: PseudoHeaderOrder) -> Self {
1087        self.pseudo_order = order;
1088        self
1089    }
1090
1091    /// Set complete timeout configuration.
1092    ///
1093    /// See [`Timeouts`] for available presets and individual timeout types.
1094    pub fn timeouts(mut self, timeouts: Timeouts) -> Self {
1095        self.timeouts = timeouts;
1096        self
1097    }
1098
1099    /// Use API-optimized timeout defaults.
1100    ///
1101    /// Equivalent to `timeouts(Timeouts::api_defaults())`.
1102    pub fn api_timeouts(mut self) -> Self {
1103        self.timeouts = Timeouts::api_defaults();
1104        self
1105    }
1106
1107    /// Use streaming-optimized timeout defaults.
1108    ///
1109    /// Equivalent to `timeouts(Timeouts::streaming_defaults())`.
1110    /// Best for SSE, chunked downloads, and other streaming responses.
1111    pub fn streaming_timeouts(mut self) -> Self {
1112        self.timeouts = Timeouts::streaming_defaults();
1113        self
1114    }
1115
1116    /// Set total request timeout (backward compatibility).
1117    ///
1118    /// This sets only the total deadline. For more granular control,
1119    /// use `timeouts()` or individual timeout setters.
1120    #[deprecated(
1121        since = "1.0.2",
1122        note = "Use `timeouts()` or `total_timeout()` instead"
1123    )]
1124    pub fn timeout(mut self, timeout: Duration) -> Self {
1125        self.timeouts.total = Some(timeout);
1126        self
1127    }
1128
1129    /// Set total request deadline timeout.
1130    pub fn total_timeout(mut self, timeout: Duration) -> Self {
1131        self.timeouts.total = Some(timeout);
1132        self
1133    }
1134
1135    /// Set connect timeout (TCP + TLS handshake).
1136    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
1137        self.timeouts.connect = Some(timeout);
1138        self
1139    }
1140
1141    /// Set TTFB (time-to-first-byte) timeout.
1142    pub fn ttfb_timeout(mut self, timeout: Duration) -> Self {
1143        self.timeouts.ttfb = Some(timeout);
1144        self
1145    }
1146
1147    /// Set read idle timeout (resets on each chunk received).
1148    pub fn read_timeout(mut self, timeout: Duration) -> Self {
1149        self.timeouts.read_idle = Some(timeout);
1150        self
1151    }
1152
1153    /// Set write idle timeout (resets on each chunk sent).
1154    pub fn write_timeout(mut self, timeout: Duration) -> Self {
1155        self.timeouts.write_idle = Some(timeout);
1156        self
1157    }
1158
1159    /// Set pool acquire timeout.
1160    pub fn pool_acquire_timeout(mut self, timeout: Duration) -> Self {
1161        self.timeouts.pool_acquire = Some(timeout);
1162        self
1163    }
1164
1165    /// Set default headers applied to every request.
1166    pub fn default_headers(mut self, headers: impl Into<Headers>) -> Self {
1167        self.default_headers = headers.into();
1168        self
1169    }
1170
1171    /// Add or replace a single default header.
1172    pub fn default_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
1173        self.default_headers.insert(name, value);
1174        self
1175    }
1176
1177    /// Convenience for setting the User-Agent default header.
1178    pub fn user_agent(mut self, value: impl Into<String>) -> Self {
1179        self.default_headers.insert("User-Agent", value.into());
1180        self
1181    }
1182
1183    /// Set redirect policy.
1184    pub fn redirect_policy(mut self, policy: RedirectPolicy) -> Self {
1185        self.redirect_policy = policy;
1186        self
1187    }
1188
1189    /// Enable or disable the cookie store.
1190    pub fn cookie_store(mut self, enabled: bool) -> Self {
1191        if enabled {
1192            self.cookie_store = Some(Arc::new(RwLock::new(CookieJar::new())));
1193        } else {
1194            self.cookie_store = None;
1195        }
1196        self
1197    }
1198
1199    /// Provide a custom cookie jar to use for requests.
1200    pub fn cookie_jar(mut self, jar: Arc<RwLock<CookieJar>>) -> Self {
1201        self.cookie_store = Some(jar);
1202        self
1203    }
1204
1205    /// Set HTTP/2 preference (for Auto version selection).
1206    pub fn prefer_http2(mut self, prefer: bool) -> Self {
1207        self.prefer_http2 = prefer;
1208        self
1209    }
1210
1211    /// Enable or disable automatic HTTP/3 upgrade via Alt-Svc headers.
1212    ///
1213    /// When enabled (default), the client will:
1214    /// 1. Parse Alt-Svc headers from HTTP/1.1 and HTTP/2 responses
1215    /// 2. Cache HTTP/3 endpoints discovered via Alt-Svc
1216    /// 3. Attempt HTTP/3 for subsequent requests when cached
1217    pub fn h3_upgrade(mut self, enabled: bool) -> Self {
1218        self.h3_upgrade_enabled = enabled;
1219        self
1220    }
1221
1222    /// Enable HTTP/2 Prior Knowledge (H2C) for cleartext connections.
1223    /// When enabled, connecting to `http://` URIs will assume HTTP/2.
1224    pub fn http2_prior_knowledge(mut self, enabled: bool) -> Self {
1225        self.http2_prior_knowledge = enabled;
1226        // Prior knowledge implies preferring H2
1227        if enabled {
1228            self.prefer_http2 = true;
1229        }
1230        self
1231    }
1232
1233    /// Add a custom root certificate (DER or PEM) to the trust store.
1234    pub fn add_root_certificate(mut self, cert: Vec<u8>) -> Self {
1235        self.root_certs.push(cert);
1236        self
1237    }
1238
1239    /// Load root certificates from the operating system's certificate store.
1240    ///
1241    /// This is REQUIRED for cross-compiled builds (e.g., building for Windows from macOS)
1242    /// because BoringSSL's default certificate store is empty when cross-compiling.
1243    ///
1244    /// On Windows, this loads certificates from the Windows Certificate Store (schannel).
1245    /// On macOS, this loads from the Keychain.
1246    /// On Linux, this loads from common certificate locations (/etc/ssl/certs, etc.).
1247    ///
1248    /// The `SSL_CERT_FILE` environment variable can override the certificate source.
1249    pub fn with_platform_roots(mut self, enabled: bool) -> Self {
1250        self.use_platform_roots = enabled;
1251        self
1252    }
1253
1254    /// Skip TLS certificate verification for all connections.
1255    ///
1256    /// # Safety
1257    /// This is DANGEROUS and should only be used for testing.
1258    /// Prefer `localhost_allows_invalid_certs(true)` for local development.
1259    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
1260        self.danger_accept_invalid_certs = accept;
1261        self
1262    }
1263
1264    /// Automatically skip TLS certificate verification for localhost connections.
1265    ///
1266    /// When enabled (default), connections to `localhost`, `127.0.0.1`, or `::1`
1267    /// will skip TLS certificate verification, making local development with
1268    /// self-signed certificates seamless.
1269    ///
1270    /// This is safe because localhost traffic never leaves the machine.
1271    pub fn localhost_allows_invalid_certs(mut self, allow: bool) -> Self {
1272        self.localhost_allows_invalid_certs = allow;
1273        self
1274    }
1275
1276    /// Build the client.
1277    pub fn build(self) -> Result<Client> {
1278        // Create connector with TLS fingerprint
1279        let tls_fingerprint = self.fingerprint.tls_fingerprint();
1280        let mut connector = BoringConnector::with_fingerprint(tls_fingerprint.clone())
1281            .with_root_certificates(self.root_certs.clone())
1282            .with_platform_roots(self.use_platform_roots);
1283
1284        // Apply global danger_accept_invalid_certs if set
1285        if self.danger_accept_invalid_certs {
1286            connector = connector.danger_accept_invalid_certs(true);
1287        }
1288
1289        // Create insecure connector for localhost (always skips TLS verification)
1290        let insecure_connector = BoringConnector::with_fingerprint(tls_fingerprint.clone())
1291            .with_root_certificates(self.root_certs)
1292            .with_platform_roots(self.use_platform_roots)
1293            .danger_accept_invalid_certs(true);
1294
1295        // Create H3 client with same TLS fingerprint
1296        let h3_client = H3Client::with_fingerprint(tls_fingerprint);
1297
1298        // Use provided HTTP/2 settings or default from fingerprint
1299        let http2_settings = self.http2_settings.unwrap_or_default();
1300
1301        // Determine default version
1302        let default_version = if self.prefer_http2 {
1303            HttpVersion::Http2
1304        } else {
1305            HttpVersion::Http1_1
1306        };
1307
1308        Ok(Client {
1309            connector,
1310            insecure_connector,
1311            h3_client,
1312            alt_svc_cache: Arc::new(AltSvcCache::new()),
1313            h2_pool: Arc::new(RwLock::new(HashMap::new())),
1314            h1_pool: Arc::new(ConnectionPool::new()),
1315            http2_settings,
1316            pseudo_order: self.pseudo_order,
1317            default_version,
1318            timeouts: self.timeouts,
1319            h3_upgrade_enabled: self.h3_upgrade_enabled,
1320            http2_prior_knowledge: self.http2_prior_knowledge,
1321            danger_accept_invalid_certs: self.danger_accept_invalid_certs,
1322            localhost_allows_invalid_certs: self.localhost_allows_invalid_certs,
1323            default_headers: self.default_headers,
1324            redirect_policy: self.redirect_policy,
1325            cookie_store: self.cookie_store,
1326        })
1327    }
1328}
1329
1330impl Default for ClientBuilder {
1331    fn default() -> Self {
1332        Self::new()
1333    }
1334}
1335
1336impl Default for AltSvcCache {
1337    fn default() -> Self {
1338        Self::new()
1339    }
1340}