|
| 1 | +//! HTTP/2 server demonstrating 103 Early Hints |
| 2 | +//! |
| 3 | +//! This example shows the recommended approach: 103 Early Hints. |
| 4 | +//! |
| 5 | +//! Run with: |
| 6 | +//! ``` |
| 7 | +//! cargo run --example http2_early_hints --features full |
| 8 | +//! ``` |
| 9 | +
|
| 10 | +use std::convert::Infallible; |
| 11 | +use std::fs; |
| 12 | +use std::net::SocketAddr; |
| 13 | +use std::time::Instant; |
| 14 | + |
| 15 | +use bytes::Bytes; |
| 16 | +use http::{Request, Response, StatusCode}; |
| 17 | +use http_body_util::Full; |
| 18 | +use hyper::body::Incoming as IncomingBody; |
| 19 | +use hyper::ext::early_hints_pusher; |
| 20 | +use hyper::server::conn::http2; |
| 21 | +use hyper::service::service_fn; |
| 22 | +use tokio::net::TcpListener; |
| 23 | +use tokio_rustls::rustls::{ |
| 24 | + pki_types::{CertificateDer, PrivateKeyDer}, |
| 25 | + ServerConfig, |
| 26 | +}; |
| 27 | +use tokio_rustls::TlsAcceptor; |
| 28 | + |
| 29 | +#[path = "../benches/support/mod.rs"] |
| 30 | +mod support; |
| 31 | +use support::{TokioExecutor, TokioIo}; |
| 32 | + |
| 33 | +/// Load certificates from provided files |
| 34 | +fn load_certificates() -> Result< |
| 35 | + (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), |
| 36 | + Box<dyn std::error::Error + Send + Sync>, |
| 37 | +> { |
| 38 | + // Read certificate file |
| 39 | + let cert_pem = fs::read_to_string("/tmp/cert.txt")?; |
| 40 | + |
| 41 | + // Parse certificate chain |
| 42 | + let mut certs = Vec::new(); |
| 43 | + for cert in rustls_pemfile::certs(&mut cert_pem.as_bytes()) { |
| 44 | + certs.push(cert?); |
| 45 | + } |
| 46 | + |
| 47 | + // Read private key file |
| 48 | + let key_pem = fs::read_to_string("/tmp/key.txt")?; |
| 49 | + |
| 50 | + // Parse private key |
| 51 | + let mut key_reader = key_pem.as_bytes(); |
| 52 | + let key = |
| 53 | + rustls_pemfile::private_key(&mut key_reader)?.ok_or("No private key found in key file")?; |
| 54 | + |
| 55 | + Ok((certs, key)) |
| 56 | +} |
| 57 | + |
| 58 | +/// Generate a self-signed certificate for testing (fallback) |
| 59 | +fn generate_self_signed_cert() -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) { |
| 60 | + use rcgen::{Certificate as RcgenCert, CertificateParams, DistinguishedName}; |
| 61 | + |
| 62 | + let mut params = CertificateParams::new(vec!["localhost".to_string()]); |
| 63 | + params.distinguished_name = DistinguishedName::new(); |
| 64 | + |
| 65 | + let cert = RcgenCert::from_params(params).unwrap(); |
| 66 | + let cert_der = cert.serialize_der().unwrap(); |
| 67 | + let private_key_der = cert.serialize_private_key_der(); |
| 68 | + |
| 69 | + ( |
| 70 | + vec![CertificateDer::from(cert_der)], |
| 71 | + PrivateKeyDer::try_from(private_key_der).unwrap(), |
| 72 | + ) |
| 73 | +} |
| 74 | + |
| 75 | +/// HTTP service demonstrating 103 Early Hints |
| 76 | +async fn handle_request( |
| 77 | + mut req: Request<IncomingBody>, |
| 78 | +) -> Result<Response<Full<Bytes>>, Infallible> { |
| 79 | + let path = req.uri().path(); |
| 80 | + println!("Received request: {} {}", req.method(), req.uri()); |
| 81 | + |
| 82 | + // Handle static resources that we hinted about |
| 83 | + match path { |
| 84 | + "/css/critical.css" | "/css/layout.css" => { |
| 85 | + return Ok(Response::builder() |
| 86 | + .status(StatusCode::OK) |
| 87 | + .header("content-type", "text/css") |
| 88 | + .body(Full::new(Bytes::from("body { font-family: sans-serif; }"))) |
| 89 | + .unwrap()); |
| 90 | + } |
| 91 | + |
| 92 | + "/js/app.js" | "/js/vendor.js" => { |
| 93 | + return Ok(Response::builder() |
| 94 | + .status(StatusCode::OK) |
| 95 | + .header("content-type", "application/javascript") |
| 96 | + .body(Full::new(Bytes::from("console.log('loaded');"))) |
| 97 | + .unwrap()); |
| 98 | + } |
| 99 | + |
| 100 | + "/fonts/main.woff2" | "/fonts/icons.woff2" => { |
| 101 | + return Ok(Response::builder() |
| 102 | + .status(StatusCode::OK) |
| 103 | + .header("content-type", "font/woff2") |
| 104 | + .body(Full::new(Bytes::from(&b"WOFF2"[..]))) |
| 105 | + .unwrap()); |
| 106 | + } |
| 107 | + |
| 108 | + "/images/hero.webp" => { |
| 109 | + return Ok(Response::builder() |
| 110 | + .status(StatusCode::OK) |
| 111 | + .header("content-type", "image/webp") |
| 112 | + .body(Full::new(Bytes::from(&b"RIFF"[..]))) |
| 113 | + .unwrap()); |
| 114 | + } |
| 115 | + |
| 116 | + // Root path - serve HTML page with all the hinted resources |
| 117 | + "/" => { |
| 118 | + // Send 103 Early Hints using the early_hints_pusher API |
| 119 | + if let Ok(mut pusher) = early_hints_pusher(&mut req) { |
| 120 | + println!("Sending 103 Early Hints (all critical resources)"); |
| 121 | + |
| 122 | + let start_time = Instant::now(); |
| 123 | + |
| 124 | + let hints = Response::builder() |
| 125 | + .status(StatusCode::EARLY_HINTS) |
| 126 | + // Critical CSS (highest priority - render blocking) |
| 127 | + .header("link", "</css/critical.css>; rel=preload; as=style") |
| 128 | + .header("link", "</css/layout.css>; rel=preload; as=style") |
| 129 | + // Critical JavaScript (high priority - interaction) |
| 130 | + .header("link", "</js/app.js>; rel=preload; as=script") |
| 131 | + .header("link", "</js/vendor.js>; rel=preload; as=script") |
| 132 | + // Fonts (medium priority - text rendering) |
| 133 | + .header( |
| 134 | + "link", |
| 135 | + "</fonts/main.woff2>; rel=preload; as=font; crossorigin", |
| 136 | + ) |
| 137 | + .header( |
| 138 | + "link", |
| 139 | + "</fonts/icons.woff2>; rel=preload; as=font; crossorigin", |
| 140 | + ) |
| 141 | + // Hero image (medium priority - above fold) |
| 142 | + .header("link", "</images/hero.webp>; rel=preload; as=image") |
| 143 | + // Metadata for tracking |
| 144 | + .header("x-resource-count", "7") |
| 145 | + .header("x-priority-order", "css,js,fonts,images") |
| 146 | + .body(()) |
| 147 | + .unwrap(); |
| 148 | + |
| 149 | + if let Err(e) = pusher.send_hints(hints).await { |
| 150 | + eprintln!("Failed to send hints: {}", e); |
| 151 | + } else { |
| 152 | + let send_duration = start_time.elapsed(); |
| 153 | + println!("103 Early Hints sent in: {:?}", send_duration); |
| 154 | + println!(" 7 resources hinted in single response"); |
| 155 | + println!(" Browser processes once, starts all preloads immediately"); |
| 156 | + } |
| 157 | + |
| 158 | + // Simulate realistic server processing time |
| 159 | + println!("Processing request (simulating database queries, template rendering...)"); |
| 160 | + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; |
| 161 | + } |
| 162 | + |
| 163 | + let html_content = r#"<!DOCTYPE html> |
| 164 | +<html> |
| 165 | +<head> |
| 166 | + <title>103 Early Hints Demo</title> |
| 167 | + <link rel="stylesheet" href="/css/critical.css"> |
| 168 | + <link rel="stylesheet" href="/css/layout.css"> |
| 169 | +</head> |
| 170 | +<body> |
| 171 | + <h1>HTTP/2 103 Early Hints</h1> |
| 172 | + <p>The resources above were hinted via 103 before this response arrived.</p> |
| 173 | + <script src="/js/app.js"></script> |
| 174 | +</body> |
| 175 | +</html>"#; |
| 176 | + |
| 177 | + return Ok(Response::builder() |
| 178 | + .status(StatusCode::OK) |
| 179 | + .header("content-type", "text/html") |
| 180 | + .body(Full::new(Bytes::from(html_content))) |
| 181 | + .unwrap()); |
| 182 | + } |
| 183 | + |
| 184 | + // Default 404 handler |
| 185 | + _ => { |
| 186 | + return Ok(Response::builder() |
| 187 | + .status(StatusCode::NOT_FOUND) |
| 188 | + .body(Full::new(Bytes::from("Not Found"))) |
| 189 | + .unwrap()); |
| 190 | + } |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +#[tokio::main] |
| 195 | +async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 196 | + // Initialize logging |
| 197 | + pretty_env_logger::init(); |
| 198 | + |
| 199 | + let addr: SocketAddr = ([0, 0, 0, 0], 3000).into(); |
| 200 | + |
| 201 | + // Load provided certificates or fallback to self-signed |
| 202 | + let (certs, key) = match load_certificates() { |
| 203 | + Ok((certs, key)) => { |
| 204 | + println!("Loaded certificates from /tmp/cert.txt and /tmp/key.txt"); |
| 205 | + (certs, key) |
| 206 | + } |
| 207 | + Err(e) => { |
| 208 | + println!( |
| 209 | + "Failed to load provided certificates ({}), generating self-signed certificate...", |
| 210 | + e |
| 211 | + ); |
| 212 | + generate_self_signed_cert() |
| 213 | + } |
| 214 | + }; |
| 215 | + |
| 216 | + // Configure TLS |
| 217 | + let mut config = ServerConfig::builder() |
| 218 | + .with_no_client_auth() |
| 219 | + .with_single_cert(certs, key)?; |
| 220 | + |
| 221 | + // Enable HTTP/2 |
| 222 | + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; |
| 223 | + |
| 224 | + let tls_acceptor = TlsAcceptor::from(std::sync::Arc::new(config)); |
| 225 | + |
| 226 | + // Create TCP listener |
| 227 | + let listener = TcpListener::bind(addr).await?; |
| 228 | + println!("103 Early Hints Server listening on https://{}", addr); |
| 229 | + println!("Test: curl -k --http2 -v https://localhost:3000/"); |
| 230 | + println!("Expected: 1 103 response + 1 final 200 response"); |
| 231 | + println!("Benefits: Minimal browser overhead, maximum performance"); |
| 232 | + |
| 233 | + loop { |
| 234 | + let (tcp_stream, _) = listener.accept().await?; |
| 235 | + let tls_acceptor = tls_acceptor.clone(); |
| 236 | + |
| 237 | + tokio::spawn(async move { |
| 238 | + // Perform TLS handshake |
| 239 | + let tls_stream = match tls_acceptor.accept(tcp_stream).await { |
| 240 | + Ok(stream) => stream, |
| 241 | + Err(e) => { |
| 242 | + eprintln!("TLS handshake failed: {}", e); |
| 243 | + return; |
| 244 | + } |
| 245 | + }; |
| 246 | + |
| 247 | + // Serve HTTP/2 connection with Early Hints support enabled |
| 248 | + let service = service_fn(handle_request); |
| 249 | + |
| 250 | + if let Err(e) = http2::Builder::new(TokioExecutor) |
| 251 | + .enable_informational() // Enable 103 Early Hints support |
| 252 | + .serve_connection(TokioIo::new(tls_stream), service) |
| 253 | + .await |
| 254 | + { |
| 255 | + eprintln!("HTTP/2 connection error: {}", e); |
| 256 | + } |
| 257 | + }); |
| 258 | + } |
| 259 | +} |
0 commit comments