Skip to main content

http_stat/
request.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// This file implements HTTP request functionality with support for HTTP/1.1, HTTP/2, and HTTP/3
16// It includes features like DNS resolution, TLS handshake, and request/response handling
17
18use super::decompress::decompress;
19use super::error::{Error, Result};
20use super::finish_with_error;
21use super::grpc::grpc_request;
22use super::net::{dns_resolve, parse_certificates, quic_connect, tcp_connect, tls_handshake};
23use super::proxy::{http_connect, socks5_connect, ProxyConfig, ProxyKind};
24use super::stats::{parse_server_timing, HttpStat, ALPN_HTTP3, FIRST_CHUNK_BYTES};
25use super::HttpRequest;
26use bytes::{Buf, Bytes, BytesMut};
27use futures::future;
28
29use http::Request;
30use http::Response;
31use http::Version;
32use http_body::{Body, Frame, SizeHint};
33use http_body_util::BodyExt;
34use hyper::body::Incoming;
35use hyper_util::rt::TokioExecutor;
36use hyper_util::rt::TokioIo;
37use std::pin::Pin;
38use std::sync::{Arc, Once, OnceLock};
39use std::task::{Context, Poll};
40use std::time::Duration;
41use std::time::Instant;
42use tokio::net::TcpStream;
43use tokio::sync::oneshot;
44use tokio::time::timeout;
45use tokio_rustls::client::TlsStream;
46
47/// Request body that records the `Instant` at which hyper finished consuming it.
48///
49/// Hyper does not expose a "request body fully sent" hook, but it does pull frames
50/// from this `Body` impl until `poll_frame` returns `Ready(None)`. We capture the
51/// timestamp at that boundary, which is the closest available signal to "last
52/// request byte handed to the transport." Used to split the new `request_send`
53/// phase from `server_processing`.
54pub(crate) struct TrackedBody {
55    data: Option<Bytes>,
56    done: Arc<OnceLock<Instant>>,
57}
58
59impl TrackedBody {
60    pub(crate) fn new(data: Bytes) -> (Self, Arc<OnceLock<Instant>>) {
61        let done = Arc::new(OnceLock::new());
62        (
63            Self {
64                data: Some(data),
65                done: done.clone(),
66            },
67            done,
68        )
69    }
70}
71
72impl Body for TrackedBody {
73    type Data = Bytes;
74    type Error = std::convert::Infallible;
75
76    fn poll_frame(
77        self: Pin<&mut Self>,
78        _cx: &mut Context<'_>,
79    ) -> Poll<Option<std::result::Result<Frame<Self::Data>, Self::Error>>> {
80        let this = self.get_mut();
81        if let Some(bytes) = this.data.take() {
82            Poll::Ready(Some(Ok(Frame::data(bytes))))
83        } else {
84            let _ = this.done.set(Instant::now());
85            Poll::Ready(None)
86        }
87    }
88
89    fn is_end_stream(&self) -> bool {
90        // Always force hyper to poll us so we can record completion.
91        false
92    }
93
94    fn size_hint(&self) -> SizeHint {
95        match &self.data {
96            Some(b) => SizeHint::with_exact(b.len() as u64),
97            None => SizeHint::with_exact(0),
98        }
99    }
100}
101
102/// Build a hyper `Request<TrackedBody>` plus the shared done-handle.
103fn build_tracked_request(
104    req: &HttpRequest,
105    is_http1: bool,
106) -> Result<(Request<TrackedBody>, Arc<OnceLock<Instant>>)> {
107    let body = req.body.clone().unwrap_or_default();
108    let (tracked, done) = TrackedBody::new(body);
109    let request = req
110        .builder(is_http1)
111        .body(tracked)
112        .map_err(|e| Error::Http { source: e })?;
113    Ok((request, done))
114}
115
116/// Split a captured `send_request` future window into request_send + server_processing
117/// using a `TrackedBody`'s completion timestamp. Falls back to lumping into
118/// server_processing if the body wasn't consumed before the response arrived
119/// (which shouldn't happen for normal request/response flows).
120fn record_send_split(
121    stat: &mut HttpStat,
122    send_start: Instant,
123    response_at: Instant,
124    done: &Arc<OnceLock<Instant>>,
125) {
126    match done.get().copied() {
127        Some(done_at) if done_at >= send_start && done_at <= response_at => {
128            stat.request_send = Some(done_at.duration_since(send_start));
129            stat.server_processing = Some(response_at.duration_since(done_at));
130        }
131        _ => {
132            stat.server_processing = Some(response_at.duration_since(send_start));
133        }
134    }
135}
136
137/// Populate `stat.server_timing` from response headers (RFC 8673).
138fn capture_server_timing(stat: &mut HttpStat, headers: &http::HeaderMap) {
139    let values: Vec<&str> = headers
140        .get_all("server-timing")
141        .iter()
142        .filter_map(|v| v.to_str().ok())
143        .collect();
144    if !values.is_empty() {
145        stat.server_timing = parse_server_timing(values.iter().copied());
146    }
147}
148
149/// Drain a streaming response body frame-by-frame, recording the moment the
150/// accumulator first crosses [`FIRST_CHUNK_BYTES`]. The returned tuple is
151/// `(body_bytes, time_to_first_100k)`. `time_to_first_100k` is `None` when
152/// the body is smaller than the threshold — there's no split to report.
153async fn drain_body_with_split(
154    body: Incoming,
155    start: Instant,
156) -> std::result::Result<(Bytes, Option<Duration>), String> {
157    let mut body = body;
158    let mut buf = BytesMut::new();
159    let mut first_chunk_at: Option<Duration> = None;
160    while let Some(frame_res) = body.frame().await {
161        let frame = frame_res.map_err(|e| format!("Failed to read response body: {e}"))?;
162        if let Ok(data) = frame.into_data() {
163            buf.extend_from_slice(&data);
164            if first_chunk_at.is_none() && buf.len() >= FIRST_CHUNK_BYTES {
165                first_chunk_at = Some(start.elapsed());
166            }
167        }
168    }
169    Ok((buf.freeze(), first_chunk_at))
170}
171
172// Initialize crypto provider once
173static INIT: Once = Once::new();
174
175fn ensure_crypto_provider() {
176    INIT.call_once(|| {
177        let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
178    });
179}
180
181// Send HTTP/1.1 request over any stream (plain TCP or TLS)
182async fn send_http1_request<S>(
183    req: Request<TrackedBody>,
184    done: Arc<OnceLock<Instant>>,
185    stream: S,
186    request_timeout: Option<Duration>,
187    tx: oneshot::Sender<String>,
188    stat: &mut HttpStat,
189) -> Result<Response<Incoming>>
190where
191    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
192{
193    let (mut sender, conn) = timeout(
194        request_timeout.unwrap_or(Duration::from_secs(30)),
195        hyper::client::conn::http1::handshake(TokioIo::new(stream)),
196    )
197    .await
198    .map_err(|e| Error::Timeout { source: e })?
199    .map_err(|e| Error::Hyper { source: e })?;
200
201    // Spawn connection task
202    tokio::spawn(async move {
203        if let Err(e) = conn.await {
204            let _ = tx.send(e.to_string());
205        }
206    });
207
208    let send_start = Instant::now();
209    let resp = sender
210        .send_request(req)
211        .await
212        .map_err(|e| Error::Hyper { source: e })?;
213    let response_at = Instant::now();
214    record_send_split(stat, send_start, response_at, &done);
215    Ok(resp)
216}
217
218// Send HTTP/2 request
219async fn send_https2_request(
220    req: Request<TrackedBody>,
221    done: Arc<OnceLock<Instant>>,
222    tls_stream: TlsStream<TcpStream>,
223    request_timeout: Option<Duration>,
224    tx: oneshot::Sender<String>,
225    stat: &mut HttpStat,
226) -> Result<Response<Incoming>> {
227    let (mut sender, conn) = timeout(
228        request_timeout.unwrap_or(Duration::from_secs(30)),
229        hyper::client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(tls_stream)),
230    )
231    .await
232    .map_err(|e| Error::Timeout { source: e })?
233    .map_err(|e| Error::Hyper { source: e })?;
234
235    // Spawn connection task
236    tokio::spawn(async move {
237        if let Err(e) = conn.await {
238            let _ = tx.send(e.to_string());
239        }
240    });
241
242    let mut req = req;
243    *req.version_mut() = hyper::Version::HTTP_2;
244    // Remove Host header for HTTP/2 as it's replaced by :authority
245    req.headers_mut().remove("Host");
246
247    let send_start = Instant::now();
248    let resp = sender
249        .send_request(req)
250        .await
251        .map_err(|e| Error::Hyper { source: e })?;
252    let response_at = Instant::now();
253    record_send_split(stat, send_start, response_at, &done);
254    Ok(resp)
255}
256
257// Handle HTTP/3 request
258async fn http3_request(http_req: HttpRequest) -> HttpStat {
259    let start = Instant::now();
260    let mut stat = HttpStat {
261        alpn: Some(ALPN_HTTP3.to_string()),
262        ..Default::default()
263    };
264
265    // DNS resolution
266    let dns_result = dns_resolve(&http_req, &mut stat).await;
267    let (addr, host) = match dns_result {
268        Ok(result) => result,
269        Err(e) => {
270            return finish_with_error(stat, e, start);
271        }
272    };
273
274    // Establish QUIC connection
275    let (client_endpoint, conn) = match timeout(
276        http_req.quic_timeout.unwrap_or(Duration::from_secs(30)),
277        quic_connect(
278            host,
279            addr,
280            http_req.skip_verify,
281            http_req.client_cert.as_deref(),
282            http_req.client_key.as_deref(),
283            http_req.bind_addr,
284            &mut stat,
285        ),
286    )
287    .await
288    {
289        Ok(Ok(result)) => result,
290        Ok(Err(e)) => {
291            return finish_with_error(stat, e, start);
292        }
293        Err(e) => {
294            return finish_with_error(stat, e, start);
295        }
296    };
297
298    // Set TLS information
299    stat.tls = Some("tls 1.3".to_string()); // QUIC always uses TLS 1.3
300    stat.alpn = Some(ALPN_HTTP3.to_string()); // We always use HTTP/3 for QUIC
301
302    // Extract certificate information
303    if let Some(peer_identity) = conn.peer_identity() {
304        if let Ok(certs) = peer_identity.downcast::<Vec<rustls::pki_types::CertificateDer>>() {
305            // Set cipher from first cert's signature algorithm (HTTP/3 specific)
306            if let Some(first_cert) = certs.first() {
307                if let Ok((_, cert)) = x509_parser::parse_x509_certificate(first_cert.as_ref()) {
308                    let oid_str = match cert.signature_algorithm.algorithm.to_string().as_str() {
309                        "1.2.840.113549.1.1.11" => "AES_256_GCM_SHA384".to_string(),
310                        "1.2.840.113549.1.1.12" => "AES_128_GCM_SHA256".to_string(),
311                        "1.2.840.113549.1.1.13" => "CHACHA20_POLY1305_SHA256".to_string(),
312                        "1.2.840.10045.4.3.2" => "AES_256_GCM_SHA384".to_string(),
313                        "1.2.840.10045.4.3.3" => "AES_128_GCM_SHA256".to_string(),
314                        "1.2.840.10045.4.3.4" => "CHACHA20_POLY1305_SHA256".to_string(),
315                        "1.3.101.112" => "AES_256_GCM_SHA384".to_string(),
316                        "1.3.101.113" => "AES_128_GCM_SHA256".to_string(),
317                        _ => format!("{:?}", cert.signature_algorithm.algorithm),
318                    };
319                    stat.cert_cipher = Some(oid_str);
320                }
321            }
322            parse_certificates(&certs, &mut stat);
323        }
324    }
325
326    // Create HTTP/3 connection
327    let quinn_conn = h3_quinn::Connection::new(conn);
328
329    let (mut driver, mut send_request) = match timeout(
330        http_req.request_timeout.unwrap_or(Duration::from_secs(30)),
331        h3::client::new(quinn_conn),
332    )
333    .await
334    {
335        Ok(Ok(result)) => result,
336        Ok(Err(e)) => {
337            return finish_with_error(stat, e, start);
338        }
339        Err(e) => {
340            return finish_with_error(stat, e, start);
341        }
342    };
343
344    // Prepare request
345    let mut req = match http_req.builder(false).body(()) {
346        Ok(req) => req,
347        Err(e) => {
348            return finish_with_error(stat, e, start);
349        }
350    };
351    *req.version_mut() = Version::HTTP_3;
352    stat.request_headers = req.headers().clone();
353    let body = http_req.body.unwrap_or_default();
354
355    // Handle connection driver
356    let drive = async move {
357        Err::<(), h3::error::ConnectionError>(future::poll_fn(|cx| driver.poll_close(cx)).await)
358    };
359
360    // Send request and handle response
361    let request = async move {
362        let mut sub_stat = HttpStat::default();
363
364        let request_send_start = Instant::now();
365        let mut stream = send_request.send_request(req).await?;
366        stream.send_data(body).await?;
367        // Finish sending — last request byte is now on the wire (or in QUIC's buffer).
368        stream.finish().await?;
369        sub_stat.request_send = Some(request_send_start.elapsed());
370
371        let server_processing_start = Instant::now();
372        let resp = stream.recv_response().await?;
373        sub_stat.server_processing = Some(server_processing_start.elapsed());
374
375        sub_stat.status = Some(resp.status());
376        sub_stat.headers = Some(resp.headers().clone());
377        capture_server_timing(&mut sub_stat, resp.headers());
378
379        // Receive response body. Capture the first-100KB instant so we can
380        // split throughput into "slow start" vs "steady state" later.
381        let content_transfer_start = Instant::now();
382        let mut buf = BytesMut::new();
383        let mut first_chunk_at: Option<Duration> = None;
384        while let Some(chunk) = stream.recv_data().await? {
385            buf.extend(chunk.chunk());
386            if first_chunk_at.is_none() && buf.len() >= FIRST_CHUNK_BYTES {
387                first_chunk_at = Some(content_transfer_start.elapsed());
388            }
389        }
390        sub_stat.content_transfer = Some(content_transfer_start.elapsed());
391        sub_stat.wire_body_size = Some(buf.len());
392        sub_stat.time_to_first_100k = first_chunk_at;
393        sub_stat.body = Some(Bytes::from(buf));
394        Ok::<HttpStat, h3::error::StreamError>(sub_stat)
395    };
396
397    // Execute request and handle results
398    let (req_res, drive_res) = tokio::join!(request, drive);
399    match req_res {
400        Ok(sub_stat) => {
401            stat.request_send = sub_stat.request_send;
402            stat.server_processing = sub_stat.server_processing;
403            stat.content_transfer = sub_stat.content_transfer;
404            stat.status = sub_stat.status;
405            stat.headers = sub_stat.headers;
406            stat.body = sub_stat.body;
407            stat.wire_body_size = sub_stat.wire_body_size;
408            stat.time_to_first_100k = sub_stat.time_to_first_100k;
409            stat.server_timing = sub_stat.server_timing;
410        }
411        Err(err) => {
412            if !err.is_h3_no_error() {
413                stat.error = Some(err.to_string());
414            }
415        }
416    }
417    if let Err(err) = drive_res {
418        if !err.is_h3_no_error() {
419            stat.error = Some(err.to_string());
420        }
421    }
422
423    stat.total = Some(start.elapsed());
424    // Close the connection immediately instead of waiting for idle
425    client_endpoint.close(0u32.into(), b"done");
426
427    stat
428}
429
430/// Connect to the effective TCP endpoint (direct or via proxy).
431/// Returns `(stream, target_host, is_http_forward_proxy, tcp_info_probe)`.
432/// - Direct: uses dns_resolve + tcp_connect, sets stat.dns_lookup / stat.addr / stat.tcp_connect.
433/// - Proxy:  connects to proxy (system DNS), sets stat.addr / stat.tcp_connect.
434///
435/// `tcp_info_probe` is a `dup(2)`'d FD pointing at the socket we'll actually
436/// use for HTTP traffic. Through a proxy the probe reflects the
437/// client-to-proxy socket, not the origin — `getsockopt(TCP_INFO)` can't see
438/// past the proxy.
439async fn tcp_via_proxy(
440    http_req: &HttpRequest,
441    stat: &mut HttpStat,
442) -> Result<(
443    TcpStream,
444    String,
445    bool,
446    Option<crate::tcp_info::TcpInfoProbe>,
447)> {
448    let uri = &http_req.uri;
449    let is_https = uri.scheme() == Some(&http::uri::Scheme::HTTPS);
450    let target_host = uri.host().unwrap_or_default().to_string();
451    let target_port = http_req.get_port();
452
453    if let Some(proxy) = http_req.proxy.as_deref().and_then(ProxyConfig::parse) {
454        let proxy_addr = format!("{}:{}", proxy.host, proxy.port);
455        let tcp_start = Instant::now();
456        let proxy_stream = timeout(
457            http_req.tcp_timeout.unwrap_or(Duration::from_secs(5)),
458            TcpStream::connect(&proxy_addr),
459        )
460        .await
461        .map_err(|e| Error::Timeout { source: e })?
462        .map_err(|e| Error::Io { source: e })?;
463
464        if let Ok(peer) = proxy_stream.peer_addr() {
465            stat.addr = Some(peer.to_string());
466        }
467
468        // Sample baseline TCP_INFO on the proxy connection (what we'll
469        // actually carry traffic over) before any SOCKS5/HTTP CONNECT bytes.
470        let (baseline, probe) = crate::tcp_info::TcpInfoProbe::capture(&proxy_stream);
471        stat.tcp_info_post_connect = baseline;
472
473        // HTTP proxy + plain HTTP target: forward mode, no tunnel
474        let is_http_forward = !is_https && matches!(proxy.kind, ProxyKind::Http);
475        let stream = if is_http_forward {
476            proxy_stream
477        } else {
478            match proxy.kind {
479                ProxyKind::Socks5 => {
480                    socks5_connect(proxy_stream, &target_host, target_port).await?
481                }
482                ProxyKind::Http => http_connect(proxy_stream, &target_host, target_port).await?,
483            }
484        };
485        stat.tcp_connect = Some(tcp_start.elapsed());
486        Ok((stream, target_host, is_http_forward, probe))
487    } else {
488        let (addr, host) = dns_resolve(http_req, stat).await?;
489        let (stream, probe) =
490            tcp_connect(addr, http_req.tcp_timeout, http_req.bind_addr, stat).await?;
491        Ok((stream, host, false, probe))
492    }
493}
494
495async fn http1_2_request(mut http_req: HttpRequest) -> HttpStat {
496    let start = Instant::now();
497    let mut stat = HttpStat::default();
498
499    let is_https = http_req.uri.scheme() == Some(&http::uri::Scheme::HTTPS);
500
501    // Establish TCP (direct or via proxy)
502    let (tcp_stream, host, is_http_forward, tcp_probe) =
503        match tcp_via_proxy(&http_req, &mut stat).await {
504            Ok(r) => r,
505            Err(e) => return finish_with_error(stat, e, start),
506        };
507
508    // HTTP forward proxy: request must use the full absolute URI
509    if is_http_forward {
510        http_req.use_absolute_uri = true;
511    }
512
513    // Create channel for connection errors
514    let (tx, mut rx) = oneshot::channel();
515
516    // Send request based on protocol
517    let resp = if is_https {
518        // TLS handshake
519        let tls_result = tls_handshake(host.clone(), tcp_stream, &http_req, &mut stat).await;
520        let (tls_stream, is_http2) = match tls_result {
521            Ok(result) => result,
522            Err(e) => {
523                return finish_with_error(stat, e, start);
524            }
525        };
526
527        // Send HTTPS request
528        if is_http2 {
529            let (req, done) = match build_tracked_request(&http_req, false) {
530                Ok(r) => r,
531                Err(e) => {
532                    return finish_with_error(stat, e, start);
533                }
534            };
535            stat.request_headers = req.headers().clone();
536            match send_https2_request(
537                req,
538                done,
539                tls_stream,
540                http_req.request_timeout,
541                tx,
542                &mut stat,
543            )
544            .await
545            {
546                Ok(resp) => resp,
547                Err(e) => {
548                    return finish_with_error(stat, e, start);
549                }
550            }
551        } else {
552            let (req, done) = match build_tracked_request(&http_req, true) {
553                Ok(r) => r,
554                Err(e) => {
555                    return finish_with_error(stat, e, start);
556                }
557            };
558            stat.request_headers = req.headers().clone();
559            match send_http1_request(
560                req,
561                done,
562                tls_stream,
563                http_req.request_timeout,
564                tx,
565                &mut stat,
566            )
567            .await
568            {
569                Ok(resp) => resp,
570                Err(e) => {
571                    return finish_with_error(stat, e, start);
572                }
573            }
574        }
575    } else {
576        let (req, done) = match build_tracked_request(&http_req, true) {
577            Ok(r) => r,
578            Err(e) => {
579                return finish_with_error(stat, e, start);
580            }
581        };
582        stat.request_headers = req.headers().clone();
583        // Send HTTP request
584        match send_http1_request(
585            req,
586            done,
587            tcp_stream,
588            http_req.request_timeout,
589            tx,
590            &mut stat,
591        )
592        .await
593        {
594            Ok(resp) => resp,
595            Err(e) => {
596                return finish_with_error(stat, e, start);
597            }
598        }
599    };
600
601    // Process response
602    stat.status = Some(resp.status());
603    stat.headers = Some(resp.headers().clone());
604    capture_server_timing(&mut stat, resp.headers());
605
606    // Check for connection errors
607    if let Ok(error) = rx.try_recv() {
608        stat.error = Some(error);
609    }
610    // Read response body — stream frame-by-frame so we can timestamp the
611    // moment 100 KiB has arrived. Combined with content_transfer, this lets
612    // us split throughput into "first 100 KB" (TCP slow-start dominated)
613    // and "tail" (steady-state server send rate).
614    let content_transfer_start = Instant::now();
615    let drain_result = drain_body_with_split(resp.into_body(), content_transfer_start).await;
616    let (body_bytes, time_to_first_100k) = match drain_result {
617        Ok(p) => p,
618        Err(e) => return finish_with_error(stat, e, start),
619    };
620    stat.wire_body_size = Some(body_bytes.len());
621    stat.time_to_first_100k = time_to_first_100k;
622    stat.body = Some(body_bytes);
623    stat.content_transfer = Some(content_transfer_start.elapsed());
624
625    // Second kernel TCP sample: retransmits accumulated during the body read,
626    // final RTT/cwnd. dup'd FD is dropped here (closes only the duplicate;
627    // the real socket lives on inside hyper).
628    if let Some(probe) = &tcp_probe {
629        stat.tcp_info_final = probe.sample();
630    }
631
632    stat.total = Some(start.elapsed());
633    stat
634}
635
636/// Performs an HTTP request and returns detailed statistics about the request lifecycle.
637///
638/// This function handles HTTP/1.1, HTTP/2, and HTTP/3 requests with the following features:
639/// - Automatic protocol selection based on ALPN negotiation
640/// - DNS resolution with support for custom IP mappings
641/// - TLS handshake with certificate verification
642/// - Response body handling with optional file output
643/// - Detailed timing statistics for each phase of the request
644///
645/// # Arguments
646///
647/// * `http_req` - An `HttpRequest` struct containing the request configuration including:
648///   - URI and HTTP method
649///   - ALPN protocols to negotiate
650///   - Custom DNS resolutions
651///   - Headers and request body
652///   - TLS verification settings
653///   - Output file path (optional)
654///
655/// # Returns
656///
657/// Returns an `HttpStat` struct containing:
658/// - DNS lookup time
659/// - QUIC connection time
660/// - TCP connection time
661/// - TLS handshake time (for HTTPS)
662/// - Server processing time
663/// - Content transfer time
664/// - Total request time
665/// - Response status and headers
666/// - Response body (if not written to file)
667/// - TLS and certificate information (for HTTPS)
668/// - Any errors that occurred during the request
669/// ```
670pub async fn request(http_req: HttpRequest) -> HttpStat {
671    ensure_crypto_provider();
672    let schema = if let Some(schema) = http_req.uri.scheme() {
673        schema.to_string()
674    } else {
675        "".to_string()
676    };
677
678    // Handle HTTP/3 request
679    let mut stat = if ["grpc", "grpcs"].contains(&schema.as_str()) {
680        grpc_request(http_req).await
681    } else if http_req.alpn_protocols.contains(&ALPN_HTTP3.to_string()) {
682        http3_request(http_req).await
683    } else {
684        http1_2_request(http_req).await
685    };
686    if let Some(body) = &stat.body {
687        stat.body_size = Some(body.len());
688    }
689    let encoding = if let Some(headers) = &stat.headers {
690        headers
691            .get("content-encoding")
692            .map(|v| v.to_str().unwrap_or_default())
693            .unwrap_or_default()
694    } else {
695        ""
696    };
697
698    if !encoding.is_empty() {
699        if let Some(body) = &stat.body {
700            match decompress(encoding, body) {
701                Ok(data) => {
702                    stat.body = Some(data);
703                }
704                Err(e) => {
705                    stat.error = Some(e.to_string());
706                }
707            }
708        }
709    }
710
711    stat
712}
713
714// --- Connection reuse API ---
715
716enum ConnectionSender {
717    Http1(hyper::client::conn::http1::SendRequest<TrackedBody>),
718    Http2(hyper::client::conn::http2::SendRequest<TrackedBody>),
719}
720
721/// A reusable HTTP connection handle for benchmarking.
722pub struct HttpConnection {
723    sender: ConnectionSender,
724    is_http2: bool,
725    /// dup'd FD so we can sample TCP_INFO after each `send()` even though
726    /// the original socket has been moved into hyper. None on non-Unix or
727    /// when `dup(2)` failed.
728    tcp_probe: Option<crate::tcp_info::TcpInfoProbe>,
729    /// Most recent TCP_INFO snapshot. Used as the "post-connect" baseline
730    /// for the next `send()`'s delta calculation, so each iteration's
731    /// `retransmits_during` reflects only that iteration's window.
732    last_tcp_info: Option<crate::TcpInfo>,
733}
734
735async fn establish_http1<S>(
736    stream: S,
737    handshake_timeout: Duration,
738    mut stat: HttpStat,
739    tcp_probe: Option<crate::tcp_info::TcpInfoProbe>,
740    start: Instant,
741) -> (HttpStat, Option<HttpConnection>)
742where
743    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
744{
745    match timeout(
746        handshake_timeout,
747        hyper::client::conn::http1::handshake(TokioIo::new(stream)),
748    )
749    .await
750    {
751        Ok(Ok((sender, conn))) => {
752            tokio::spawn(async move {
753                let _ = conn.await;
754            });
755            stat.total = Some(start.elapsed());
756            let last_tcp_info = stat.tcp_info_post_connect.clone();
757            (
758                stat,
759                Some(HttpConnection {
760                    sender: ConnectionSender::Http1(sender),
761                    is_http2: false,
762                    tcp_probe,
763                    last_tcp_info,
764                }),
765            )
766        }
767        Ok(Err(e)) => (
768            finish_with_error(stat, Error::Hyper { source: e }, start),
769            None,
770        ),
771        Err(e) => (
772            finish_with_error(stat, Error::Timeout { source: e }, start),
773            None,
774        ),
775    }
776}
777
778async fn establish_http2<S>(
779    stream: S,
780    handshake_timeout: Duration,
781    mut stat: HttpStat,
782    tcp_probe: Option<crate::tcp_info::TcpInfoProbe>,
783    start: Instant,
784) -> (HttpStat, Option<HttpConnection>)
785where
786    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
787{
788    match timeout(
789        handshake_timeout,
790        hyper::client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(stream)),
791    )
792    .await
793    {
794        Ok(Ok((sender, conn))) => {
795            tokio::spawn(async move {
796                let _ = conn.await;
797            });
798            stat.total = Some(start.elapsed());
799            let last_tcp_info = stat.tcp_info_post_connect.clone();
800            (
801                stat,
802                Some(HttpConnection {
803                    sender: ConnectionSender::Http2(sender),
804                    is_http2: true,
805                    tcp_probe,
806                    last_tcp_info,
807                }),
808            )
809        }
810        Ok(Err(e)) => (
811            finish_with_error(stat, Error::Hyper { source: e }, start),
812            None,
813        ),
814        Err(e) => (
815            finish_with_error(stat, Error::Timeout { source: e }, start),
816            None,
817        ),
818    }
819}
820
821/// Establish an HTTP/1.1 or HTTP/2 connection and return a reusable handle.
822///
823/// Returns `(connect_stat, Some(conn))` on success, or `(error_stat, None)` on failure.
824/// Only supports HTTP/1.1 and HTTP/2. For HTTP/3 or gRPC, use `request()` directly.
825pub async fn connect(http_req: &HttpRequest) -> (HttpStat, Option<HttpConnection>) {
826    ensure_crypto_provider();
827    let start = Instant::now();
828    let mut stat = HttpStat::default();
829
830    let is_https = http_req.uri.scheme() == Some(&http::uri::Scheme::HTTPS);
831
832    let (tcp_stream, host, _is_http_forward, tcp_probe) =
833        match tcp_via_proxy(http_req, &mut stat).await {
834            Ok(r) => r,
835            Err(e) => return (finish_with_error(stat, e, start), None),
836        };
837
838    let handshake_timeout = http_req.request_timeout.unwrap_or(Duration::from_secs(30));
839
840    if is_https {
841        let (tls_stream, is_h2) = match tls_handshake(host, tcp_stream, http_req, &mut stat).await {
842            Ok(r) => r,
843            Err(e) => return (finish_with_error(stat, e, start), None),
844        };
845
846        if is_h2 {
847            establish_http2(tls_stream, handshake_timeout, stat, tcp_probe, start).await
848        } else {
849            establish_http1(tls_stream, handshake_timeout, stat, tcp_probe, start).await
850        }
851    } else {
852        establish_http1(tcp_stream, handshake_timeout, stat, tcp_probe, start).await
853    }
854}
855
856impl HttpConnection {
857    /// Send a request on the existing connection, returning only request-phase timing.
858    pub async fn send(&mut self, http_req: &HttpRequest) -> HttpStat {
859        let start = Instant::now();
860        // Seed the per-iteration TCP_INFO baseline from the previous send's
861        // final sample (or the connection's post-connect snapshot for the
862        // first iteration). This way each iteration's retransmits_during
863        // counts only retransmits in *this* iteration's window.
864        let mut stat = HttpStat {
865            tcp_info_post_connect: self.last_tcp_info.clone(),
866            ..HttpStat::default()
867        };
868
869        let is_http1 = !self.is_http2;
870        let (req, done) = match build_tracked_request(http_req, is_http1) {
871            Ok(r) => r,
872            Err(e) => return finish_with_error(stat, e, start),
873        };
874        stat.request_headers = req.headers().clone();
875
876        // Ensure the connection is ready (especially important for HTTP/1.1 keep-alive)
877        match &mut self.sender {
878            ConnectionSender::Http1(sender) => {
879                if let Err(e) = sender.ready().await {
880                    return finish_with_error(stat, Error::Hyper { source: e }, start);
881                }
882            }
883            ConnectionSender::Http2(sender) => {
884                if let Err(e) = sender.ready().await {
885                    return finish_with_error(stat, Error::Hyper { source: e }, start);
886                }
887            }
888        }
889
890        let send_start = Instant::now();
891        let resp = match &mut self.sender {
892            ConnectionSender::Http1(sender) => sender.send_request(req).await,
893            ConnectionSender::Http2(sender) => {
894                let mut req = req;
895                *req.version_mut() = Version::HTTP_2;
896                req.headers_mut().remove("Host");
897                sender.send_request(req).await
898            }
899        };
900
901        let resp = match resp {
902            Ok(resp) => resp,
903            Err(e) => return finish_with_error(stat, Error::Hyper { source: e }, start),
904        };
905        let response_at = Instant::now();
906        record_send_split(&mut stat, send_start, response_at, &done);
907        stat.status = Some(resp.status());
908        stat.headers = Some(resp.headers().clone());
909        capture_server_timing(&mut stat, resp.headers());
910
911        // Read response body — frame-by-frame so we capture the
912        // time-to-first-100K marker for throughput-split diagnosis (matches
913        // the http1_2_request path).
914        let content_transfer_start = Instant::now();
915        match drain_body_with_split(resp.into_body(), content_transfer_start).await {
916            Ok((body_bytes, first_100k)) => {
917                stat.wire_body_size = Some(body_bytes.len());
918                stat.time_to_first_100k = first_100k;
919                stat.body = Some(body_bytes);
920                stat.content_transfer = Some(content_transfer_start.elapsed());
921            }
922            Err(e) => {
923                return finish_with_error(stat, e, start);
924            }
925        }
926
927        // End-of-iteration kernel TCP snapshot. Cache it as the baseline for
928        // the next send() so successive iterations don't double-count
929        // retransmits.
930        if let Some(probe) = &self.tcp_probe {
931            let now = probe.sample();
932            stat.tcp_info_final = now.clone();
933            if now.is_some() {
934                self.last_tcp_info = now;
935            }
936        }
937
938        stat.total = Some(start.elapsed());
939
940        // Handle decompression
941        if let Some(body) = &stat.body {
942            stat.body_size = Some(body.len());
943        }
944        let encoding = stat
945            .headers
946            .as_ref()
947            .and_then(|h| h.get("content-encoding"))
948            .and_then(|v| v.to_str().ok())
949            .unwrap_or_default();
950        if !encoding.is_empty() {
951            if let Some(body) = &stat.body {
952                match decompress(encoding, body) {
953                    Ok(data) => stat.body = Some(data),
954                    Err(e) => stat.error = Some(e.to_string()),
955                }
956            }
957        }
958
959        stat
960    }
961}