Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ tokio = { version = "1", features = [
] }
tokio-test = "0.4"
tokio-util = "0.7.10"
# Additional dependencies for HTTP/2 Early Hints example
rcgen = "0.12"
tokio-rustls = "0.25"
rustls-pemfile = "2.0"

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

# Client/Server
client = ["dep:want", "dep:pin-project-lite", "dep:smallvec"]
server = ["dep:httpdate", "dep:pin-project-lite", "dep:smallvec"]
server = ["dep:httpdate", "dep:pin-project-lite", "dep:smallvec", "dep:futures-util"]

# C-API support (currently unstable (no semver))
ffi = ["dep:http-body-util", "dep:futures-util"]
Expand Down Expand Up @@ -305,6 +309,11 @@ name = "web_api"
path = "examples/web_api.rs"
required-features = ["full"]

[[example]]
name = "http2_early_hints"
path = "examples/http2_early_hints.rs"
required-features = ["full"]


[[bench]]
name = "body"
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ futures-util = { version = "0.3", default-features = false }

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

* [`http2_early_hints`](http2_early_hints.rs) - An HTTP/2 server that sends 103 Early Hints.

## Going Further

* [`gateway`](gateway.rs) - A server gateway (reverse proxy) that proxies to the `hello` service above.
Expand Down
259 changes: 259 additions & 0 deletions examples/http2_early_hints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
//! HTTP/2 server demonstrating 103 Early Hints
//!
//! This example shows the recommended approach: 103 Early Hints.
//!
//! Run with:
//! ```
//! cargo run --example http2_early_hints --features full
//! ```

use std::convert::Infallible;
use std::fs;
use std::net::SocketAddr;
use std::time::Instant;

use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body_util::Full;
use hyper::body::Incoming as IncomingBody;
use hyper::ext::early_hints_pusher;
use hyper::server::conn::http2;
use hyper::service::service_fn;
use tokio::net::TcpListener;
use tokio_rustls::rustls::{
pki_types::{CertificateDer, PrivateKeyDer},
ServerConfig,
};
use tokio_rustls::TlsAcceptor;

#[path = "../benches/support/mod.rs"]
mod support;
use support::{TokioExecutor, TokioIo};

/// Load certificates from provided files
fn load_certificates() -> Result<
(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
Box<dyn std::error::Error + Send + Sync>,
> {
// Read certificate file
let cert_pem = fs::read_to_string("/tmp/cert.txt")?;

// Parse certificate chain
let mut certs = Vec::new();
for cert in rustls_pemfile::certs(&mut cert_pem.as_bytes()) {
certs.push(cert?);
}

// Read private key file
let key_pem = fs::read_to_string("/tmp/key.txt")?;

// Parse private key
let mut key_reader = key_pem.as_bytes();
let key =
rustls_pemfile::private_key(&mut key_reader)?.ok_or("No private key found in key file")?;

Ok((certs, key))
}

/// Generate a self-signed certificate for testing (fallback)
fn generate_self_signed_cert() -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) {
use rcgen::{Certificate as RcgenCert, CertificateParams, DistinguishedName};

let mut params = CertificateParams::new(vec!["localhost".to_string()]);
params.distinguished_name = DistinguishedName::new();

let cert = RcgenCert::from_params(params).unwrap();
let cert_der = cert.serialize_der().unwrap();
let private_key_der = cert.serialize_private_key_der();

(
vec![CertificateDer::from(cert_der)],
PrivateKeyDer::try_from(private_key_der).unwrap(),
)
}

/// HTTP service demonstrating 103 Early Hints
async fn handle_request(
mut req: Request<IncomingBody>,
) -> Result<Response<Full<Bytes>>, Infallible> {
let path = req.uri().path();
println!("Received request: {} {}", req.method(), req.uri());

// Handle static resources that we hinted about
match path {
"/css/critical.css" | "/css/layout.css" => {
return Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/css")
.body(Full::new(Bytes::from("body { font-family: sans-serif; }")))
.unwrap());
}

"/js/app.js" | "/js/vendor.js" => {
return Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/javascript")
.body(Full::new(Bytes::from("console.log('loaded');")))
.unwrap());
}

"/fonts/main.woff2" | "/fonts/icons.woff2" => {
return Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "font/woff2")
.body(Full::new(Bytes::from(&b"WOFF2"[..])))
.unwrap());
}

"/images/hero.webp" => {
return Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "image/webp")
.body(Full::new(Bytes::from(&b"RIFF"[..])))
.unwrap());
}

// Root path - serve HTML page with all the hinted resources
"/" => {
// Send 103 Early Hints using the early_hints_pusher API
if let Ok(mut pusher) = early_hints_pusher(&mut req) {
println!("Sending 103 Early Hints (all critical resources)");

let start_time = Instant::now();

let hints = Response::builder()
.status(StatusCode::EARLY_HINTS)
// Critical CSS (highest priority - render blocking)
.header("link", "</css/critical.css>; rel=preload; as=style")
.header("link", "</css/layout.css>; rel=preload; as=style")
// Critical JavaScript (high priority - interaction)
.header("link", "</js/app.js>; rel=preload; as=script")
.header("link", "</js/vendor.js>; rel=preload; as=script")
// Fonts (medium priority - text rendering)
.header(
"link",
"</fonts/main.woff2>; rel=preload; as=font; crossorigin",
)
.header(
"link",
"</fonts/icons.woff2>; rel=preload; as=font; crossorigin",
)
// Hero image (medium priority - above fold)
.header("link", "</images/hero.webp>; rel=preload; as=image")
// Metadata for tracking
.header("x-resource-count", "7")
.header("x-priority-order", "css,js,fonts,images")
.body(())
.unwrap();

if let Err(e) = pusher.send_hints(hints).await {
eprintln!("Failed to send hints: {}", e);
} else {
let send_duration = start_time.elapsed();
println!("103 Early Hints sent in: {:?}", send_duration);
println!(" 7 resources hinted in single response");
println!(" Browser processes once, starts all preloads immediately");
}

// Simulate realistic server processing time
println!("Processing request (simulating database queries, template rendering...)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
}

let html_content = r#"<!DOCTYPE html>
<html>
<head>
<title>103 Early Hints Demo</title>
<link rel="stylesheet" href="/css/critical.css">
<link rel="stylesheet" href="/css/layout.css">
</head>
<body>
<h1>HTTP/2 103 Early Hints</h1>
<p>The resources above were hinted via 103 before this response arrived.</p>
<script src="/js/app.js"></script>
</body>
</html>"#;

return Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html")
.body(Full::new(Bytes::from(html_content)))
.unwrap());
}

// Default 404 handler
_ => {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Full::new(Bytes::from("Not Found")))
.unwrap());
}
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Initialize logging
pretty_env_logger::init();

let addr: SocketAddr = ([0, 0, 0, 0], 3000).into();

// Load provided certificates or fallback to self-signed
let (certs, key) = match load_certificates() {
Ok((certs, key)) => {
println!("Loaded certificates from /tmp/cert.txt and /tmp/key.txt");
(certs, key)
}
Err(e) => {
println!(
"Failed to load provided certificates ({}), generating self-signed certificate...",
e
);
generate_self_signed_cert()
}
};

// Configure TLS
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)?;

// Enable HTTP/2
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

let tls_acceptor = TlsAcceptor::from(std::sync::Arc::new(config));

// Create TCP listener
let listener = TcpListener::bind(addr).await?;
println!("103 Early Hints Server listening on https://{}", addr);
println!("Test: curl -k --http2 -v https://localhost:3000/");
println!("Expected: 1 103 response + 1 final 200 response");
println!("Benefits: Minimal browser overhead, maximum performance");

loop {
let (tcp_stream, _) = listener.accept().await?;
let tls_acceptor = tls_acceptor.clone();

tokio::spawn(async move {
// Perform TLS handshake
let tls_stream = match tls_acceptor.accept(tcp_stream).await {
Ok(stream) => stream,
Err(e) => {
eprintln!("TLS handshake failed: {}", e);
return;
}
};

// Serve HTTP/2 connection with Early Hints support enabled
let service = service_fn(handle_request);

if let Err(e) = http2::Builder::new(TokioExecutor)
.enable_informational() // Enable 103 Early Hints support
.serve_connection(TokioIo::new(tls_stream), service)
.await
{
eprintln!("HTTP/2 connection error: {}", e);
}
});
}
}
58 changes: 56 additions & 2 deletions src/client/conn/http2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use futures_core::ready;
use http::{Request, Response};

use super::super::dispatch::{self, TrySendError};
use super::informational::InformationalConfig;
use crate::body::{Body, Incoming as IncomingBody};
use crate::common::time::Time;
use crate::proto;
Expand Down Expand Up @@ -68,6 +69,7 @@ pub struct Builder<Ex> {
pub(super) exec: Ex,
pub(super) timer: Time,
h2_builder: proto::h2::client::Config,
informational_config: InformationalConfig,
}

/// Returns a handshake future over some IO.
Expand Down Expand Up @@ -299,6 +301,7 @@ where
exec,
timer: Time::Empty,
h2_builder: proto::h2::client::Config::default(),
informational_config: InformationalConfig::new(),
}
}

Expand Down Expand Up @@ -542,6 +545,50 @@ where
self
}

/// Configures handling of informational responses (1xx status codes).
///
/// By default, informational responses are ignored. This method allows you to
/// provide a callback that will be invoked whenever an informational response
/// is received, such as 103 Early Hints.
///
/// # Examples
///
/// ```rust
/// use hyper::client::conn::http2::Builder;
/// use hyper::client::conn::informational::InformationalConfig;
/// use http::StatusCode;
///
/// #[derive(Clone)]
/// struct TokioExecutor;
///
/// impl<F> hyper::rt::Executor<F> for TokioExecutor
/// where
/// F: std::future::Future + Send + 'static,
/// F::Output: Send + 'static,
/// {
/// fn execute(&self, fut: F) {
/// tokio::task::spawn(fut);
/// }
/// }
///
/// let mut builder = Builder::new(TokioExecutor);
/// builder.informational_responses(
/// InformationalConfig::new().with_callback(|response| {
/// if response.status() == StatusCode::EARLY_HINTS {
/// println!("Received 103 Early Hints");
/// // Process Link headers for resource preloading
/// for link in response.headers().get_all("link") {
/// println!("Preload: {:?}", link);
/// }
/// }
/// })
/// );
/// ```
pub fn informational_responses(&mut self, config: InformationalConfig) -> &mut Self {
self.informational_config = config;
self
}

/// Constructs a connection with the configured options and IO.
/// See [`client::conn`](crate::client::conn) for more.
///
Expand All @@ -564,8 +611,15 @@ where
trace!("client handshake HTTP/2");

let (tx, rx) = dispatch::channel();
let h2 = proto::h2::client::handshake(io, rx, &opts.h2_builder, opts.exec, opts.timer)
.await?;
let h2 = proto::h2::client::handshake(
io,
rx,
&opts.h2_builder,
opts.exec,
opts.timer,
Some(opts.informational_config.clone()),
)
.await?;
Ok((
SendRequest {
dispatch: tx.unbound(),
Expand Down
Loading