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