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