Frontend Labs
⋯

64. Capstone: one HTTPS request, every layer, every guide, in Rust

The HTTPS guide's final chapter traces one request through the whole series in Go. This is the Rust version: a curl -w-style tool that performs each phase by hand, times it, and prints which guide explains it.

// src/bin/lifecycle.rs     usage: lifecycle https://example.com/
use rustls::pki_types::ServerName;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::time::timeout;

fn phase(name: &str, took: Duration, detail: &str, guide: &str) {
    println!("{name:<10} {:>9.2?}  {detail:<55} {guide}", took);
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let raw = std::env::args().nth(1).unwrap_or_else(|| "https://example.com/".into());
    let url = reqwest::Url::parse(&raw)?;
    anyhow::ensure!(url.scheme() == "https", "https URLs only");
    let host = url.host_str().ok_or_else(|| anyhow::anyhow!("no host"))?.to_string();
    let port = url.port_or_known_default().unwrap_or(443);
    let path = match url.query() { Some(q) => format!("{}?{q}", url.path()), None => url.path().to_string() };
    let total = Instant::now();

    // 1. DNS: name -> addresses (Net Ch 18, HTTPS Ch 2). The OS resolver reads /etc/hosts first (Ch 15).
    let t = Instant::now();
    let addrs: Vec<_> = timeout(Duration::from_secs(5), tokio::net::lookup_host((host.as_str(), port))).await??.collect();
    let addr = *addrs.iter().find(|a| a.is_ipv4()).or(addrs.first()).ok_or_else(|| anyhow::anyhow!("no addresses"))?;
    phase("dns", t.elapsed(), &format!("{host} -> {addr} ({} addresses)", addrs.len()), "Net Ch 18 · HTTPS Ch 2 · Rust Ch 45");

    // 2. TCP: three-way handshake = one round trip (Net Ch 21, HTTPS Ch 3). Measures RTT to the edge.
    let t = Instant::now();
    let tcp = timeout(Duration::from_secs(5), TcpStream::connect(addr)).await??;
    tcp.set_nodelay(true)?;
    phase("tcp", t.elapsed(), &format!("{} -> {}", tcp.local_addr()?, tcp.peer_addr()?), "Net Ch 21 · OS Ch 19 · Rust Ch 32");

    // 3. TLS 1.3: one more round trip; certificate chain validated against the Mozilla root set.
    let t = Instant::now();
    let mut roots = rustls::RootCertStore::empty();
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
    let mut cfg = rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
    cfg.alpn_protocols = vec![b"http/1.1".to_vec()];             // we speak HTTP/1.1 by hand below
    let connector = tokio_rustls::TlsConnector::from(Arc::new(cfg));
    let mut tls = timeout(Duration::from_secs(5), connector.connect(ServerName::try_from(host.clone())?, tcp)).await??;
    let (_, conn) = tls.get_ref();
    let detail = format!("{:?}, {:?}, ALPN {:?}, chain of {}",
        conn.protocol_version().unwrap(), conn.negotiated_cipher_suite().unwrap().suite(),
        conn.alpn_protocol().map(String::from_utf8_lossy), conn.peer_certificates().map_or(0, |c| c.len()));
    phase("tls", t.elapsed(), &detail, "Sec Ch 28-29 · HTTPS Ch 4 · Rust Ch 49");

    // 4. HTTP/1.1 request; time to first byte = network RTT + server (and CDN/proxy) processing.
    let t = Instant::now();
    let req = format!("GET {path} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: rust-guide-lifecycle/1.0\r\nAccept: */*\r\nConnection: close\r\n\r\n");
    tls.write_all(req.as_bytes()).await?;
    let mut reader = BufReader::new(tls);
    let mut status = String::new();
    timeout(Duration::from_secs(10), reader.read_line(&mut status)).await??;
    phase("ttfb", t.elapsed(), status.trim(), "Net Ch 28 · HTTPS Ch 5 · Rust Ch 46");

    // 5. Response headers: what the infrastructure in between left behind.
    let mut interesting = Vec::new();
    loop {
        let mut line = String::new();
        if reader.read_line(&mut line).await? == 0 || line == "\r\n" { break; }
        let lower = line.to_ascii_lowercase();
        for h in ["server:", "via:", "age:", "cache-control:", "cf-cache-status:", "x-cache:", "alt-svc:", "strict-transport-security:"] {
            if lower.starts_with(h) { interesting.push(line.trim().to_string()); }
        }
    }
    for h in &interesting { println!("{:<21}{h}", ""); }
    println!("{:<21}(proxies, CDN cache: HTTPS Ch 8, 10, 15 · HSTS: HTTPS Ch 12 · HTTP/3 via Alt-Svc: HTTPS Ch 14)", "");

    // 6. Body: read to EOF (Connection: close), bounded.
    let t = Instant::now();
    let mut body = Vec::new();
    let mut limited = tokio::io::AsyncReadExt::take(reader, 10 << 20);
    tokio::io::AsyncReadExt::read_to_end(&mut limited, &mut body).await?;
    phase("body", t.elapsed(), &format!("{} bytes", body.len()), "OS Ch 14 (bytes land in a socket buffer, then your Vec)");
    phase("total", total.elapsed(), "", "");
    Ok(())
}
cargo run --release --bin lifecycle -- https://example.com/
dns          77.95ms  example.com -> 172.66.147.243:443 (2 addresses)         Net Ch 18 · HTTPS Ch 2 · Rust Ch 45
tcp          14.24ms  192.168.1.4:55293 -> 172.66.147.243:443                 Net Ch 21 · OS Ch 19 · Rust Ch 32
tls          62.97ms  TLSv1_3, TLS13_AES_256_GCM_SHA384, ALPN Some("http/1.1"), chain of 4 Sec Ch 28-29 · HTTPS Ch 4 · Rust Ch 49
ttfb         18.36ms  HTTP/1.1 200 OK                                         Net Ch 28 · HTTPS Ch 5 · Rust Ch 46
                     Server: cloudflare
                     Age: 12297
                     cf-cache-status: HIT
                     alt-svc: h3=":443"; ma=86400
                     (proxies, CDN cache: HTTPS Ch 8, 10, 15 · HSTS: HTTPS Ch 12 · HTTP/3 via Alt-Svc: HTTPS Ch 14)
body          9.04µs  589 bytes                                               OS Ch 14 (bytes land in a socket buffer, then your Vec)
total       173.64ms

Reading your own numbers. The run above was a debug build on a home connection, and yours will differ. Here, cf-cache-status: HIT and Age: 12297 mean a Cloudflare edge served the page from cache, so ttfb (18 ms) is about one round trip, the same as tcp (14 ms). The alt-svc header advertises HTTP/3 for the next visit.

  • tcp ≈ one RTT, and tls ≈ one more RTT plus crypto. Here tls is about four times tcp. Part of that is the unoptimized debug build doing the signature checks, so rerun with --release before blaming the network. If it stays high, look at certificate chain size or a TLS terminator far from the edge.
  • ttfb minus one RTT is server time. On a CDN hit, as above, it is close to zero. On a miss it includes the trip to the origin (HTTPS Ch 10).
  • The dns phase is often near zero on the second run because the OS or the resolver cached it (Net Ch 18).
  • Debug a slow phase with the tools from HTTPS Ch 17: dig for dns, tcpdump/ss -ti for tcp (Net Ch 31, 33), and openssl s_client for tls.

Every phase in this program is one of the guides. DNS and TCP come from the networking guide. The socket and the bytes in the kernel buffer come from the OS guide. Certificate validation and AEAD record encryption come from the security guides. Status codes, headers, and caches come from the HTTPS guide. The program is about 100 lines because Rust's libraries handle each layer correctly and the types keep the layers apart.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36