Skip to content

Commit 787b37a

Browse files
Apu Islamapu031
authored andcommitted
feat(http2): implement 103 Early Hints support
Add complete HTTP/2 103 Early Hints implementation with client and server support: - Add InformationalSender extension for server-side hint transmission via mpsc channel - Create InformationalCallback system for client-side informational response handling - Extend HTTP/2 client builder with informational_responses() configuration method - Implement informational response polling in h2 client task with callback invocation - Add server-side informational response forwarding using h2's send_informational API - Include extensive integration tests covering multiple scenarios and edge cases - Add `enable_informational` config field (defaults to false) and conditional channel creation per request - Add complete working example with TLS, resource preloading, and performance monitoring - Update Cargo.toml with local h2 dependency and example build configuration The implementation enables servers to send resource preload hints before final responses, allowing browsers to start downloading critical resources early and improve page load performance. Clients can register callbacks to process 103 Early Hints and other informational responses. Closes #3980, #2426
1 parent ff5a371 commit 787b37a

14 files changed

Lines changed: 1257 additions & 9 deletions

File tree

Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ tokio = { version = "1", features = [
6767
] }
6868
tokio-test = "0.4"
6969
tokio-util = "0.7.10"
70+
# Additional dependencies for HTTP/2 Early Hints example
71+
rcgen = "0.12"
72+
tokio-rustls = "0.25"
73+
rustls-pemfile = "2.0"
7074

7175
[features]
7276
# Nothing by default
@@ -86,7 +90,7 @@ http2 = ["dep:atomic-waker", "dep:futures-channel", "dep:futures-core", "dep:h2"
8690

8791
# Client/Server
8892
client = ["dep:want", "dep:pin-project-lite", "dep:smallvec"]
89-
server = ["dep:httpdate", "dep:pin-project-lite", "dep:smallvec"]
93+
server = ["dep:httpdate", "dep:pin-project-lite", "dep:smallvec", "dep:futures-util"]
9094

9195
# C-API support (currently unstable (no semver))
9296
ffi = ["dep:http-body-util", "dep:futures-util"]
@@ -304,6 +308,11 @@ name = "web_api"
304308
path = "examples/web_api.rs"
305309
required-features = ["full"]
306310

311+
[[example]]
312+
name = "http2_early_hints"
313+
path = "examples/http2_early_hints.rs"
314+
required-features = ["full"]
315+
307316

308317
[[bench]]
309318
name = "body"

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ futures-util = { version = "0.3", default-features = false }
3838

3939
* [`echo`](echo.rs) - An echo server that copies POST request's content to the response content.
4040

41+
* [`http2_early_hints`](http2_early_hints.rs) - An HTTP/2 server that sends 103 Early Hints.
42+
4143
## Going Further
4244

4345
* [`gateway`](gateway.rs) - A server gateway (reverse proxy) that proxies to the `hello` service above.

examples/http2_early_hints.rs

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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+
}

src/client/conn/http2.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use futures_core::ready;
1414
use http::{Request, Response};
1515

1616
use super::super::dispatch::{self, TrySendError};
17+
use super::informational::InformationalConfig;
1718
use crate::body::{Body, Incoming as IncomingBody};
1819
use crate::common::time::Time;
1920
use crate::proto;
@@ -68,6 +69,7 @@ pub struct Builder<Ex> {
6869
pub(super) exec: Ex,
6970
pub(super) timer: Time,
7071
h2_builder: proto::h2::client::Config,
72+
informational_config: InformationalConfig,
7173
}
7274

7375
/// Returns a handshake future over some IO.
@@ -299,6 +301,7 @@ where
299301
exec,
300302
timer: Time::Empty,
301303
h2_builder: proto::h2::client::Config::default(),
304+
informational_config: InformationalConfig::new(),
302305
}
303306
}
304307

@@ -542,6 +545,50 @@ where
542545
self
543546
}
544547

548+
/// Configures handling of informational responses (1xx status codes).
549+
///
550+
/// By default, informational responses are ignored. This method allows you to
551+
/// provide a callback that will be invoked whenever an informational response
552+
/// is received, such as 103 Early Hints.
553+
///
554+
/// # Examples
555+
///
556+
/// ```rust
557+
/// use hyper::client::conn::http2::Builder;
558+
/// use hyper::client::conn::informational::InformationalConfig;
559+
/// use http::StatusCode;
560+
///
561+
/// #[derive(Clone)]
562+
/// struct TokioExecutor;
563+
///
564+
/// impl<F> hyper::rt::Executor<F> for TokioExecutor
565+
/// where
566+
/// F: std::future::Future + Send + 'static,
567+
/// F::Output: Send + 'static,
568+
/// {
569+
/// fn execute(&self, fut: F) {
570+
/// tokio::task::spawn(fut);
571+
/// }
572+
/// }
573+
///
574+
/// let mut builder = Builder::new(TokioExecutor);
575+
/// builder.informational_responses(
576+
/// InformationalConfig::new().with_callback(|response| {
577+
/// if response.status() == StatusCode::EARLY_HINTS {
578+
/// println!("Received 103 Early Hints");
579+
/// // Process Link headers for resource preloading
580+
/// for link in response.headers().get_all("link") {
581+
/// println!("Preload: {:?}", link);
582+
/// }
583+
/// }
584+
/// })
585+
/// );
586+
/// ```
587+
pub fn informational_responses(&mut self, config: InformationalConfig) -> &mut Self {
588+
self.informational_config = config;
589+
self
590+
}
591+
545592
/// Constructs a connection with the configured options and IO.
546593
/// See [`client::conn`](crate::client::conn) for more.
547594
///
@@ -564,8 +611,15 @@ where
564611
trace!("client handshake HTTP/2");
565612

566613
let (tx, rx) = dispatch::channel();
567-
let h2 = proto::h2::client::handshake(io, rx, &opts.h2_builder, opts.exec, opts.timer)
568-
.await?;
614+
let h2 = proto::h2::client::handshake(
615+
io,
616+
rx,
617+
&opts.h2_builder,
618+
opts.exec,
619+
opts.timer,
620+
Some(opts.informational_config.clone()),
621+
)
622+
.await?;
569623
Ok((
570624
SendRequest {
571625
dispatch: tx.unbound(),

0 commit comments

Comments
 (0)