Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Experimental HTTP/3 Support #1599

Merged
merged 30 commits into from Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9896862
Add and hook up simplistic HTTP/3 Client
kckeiks Jul 29, 2022
bcf2ddd
Pass Rustls config from Builder to H3 client constructor
kckeiks Jul 30, 2022
797edbf
Add Pool code from Hyper
kckeiks Aug 4, 2022
5dd0e87
Add connection pool to HTTP/3 Client
kckeiks Aug 4, 2022
107372c
Construct http::Body from chunks when creating response
kckeiks Aug 5, 2022
808c0cc
Add feature flags to example
kckeiks Aug 5, 2022
d62d538
Clean up code
kckeiks Aug 5, 2022
57c6ab8
Add HTTP/3-client builder
kckeiks Aug 5, 2022
4a5f526
Use feature flag when creating Request for hyper and h3 clients
kckeiks Aug 6, 2022
eb66d19
Make HTTP/3 dependencies optional
kckeiks Aug 6, 2022
1b68cfc
Add cfg to HTTP version enum
kckeiks Aug 6, 2022
8e3819e
Send payload data in requests
kckeiks Aug 6, 2022
4c04036
Change visivility of HTTP/3-client builder
kckeiks Aug 9, 2022
d79fac4
Use the same type of request when resending it
kckeiks Aug 9, 2022
040384d
Add HTTP/3 connector that reuses DNS resolvers
kckeiks Aug 10, 2022
9286bbb
Add Resolver enum to hold different kinds of resolvers
kckeiks Aug 11, 2022
1fc2fb1
Add trust-dns resolvers for HTTP/3
kckeiks Aug 11, 2022
08e46af
Move connector to its own module
kckeiks Aug 11, 2022
7931f0c
Add setter for enabling early data in TLS 1.3
kckeiks Aug 11, 2022
b102950
Make pooling more robust
kckeiks Aug 12, 2022
26a14b8
Add setters to configure some QUIC parameters
kckeiks Aug 13, 2022
35ed469
Remove client builder
kckeiks Aug 13, 2022
fe36257
Prevent multiple QUIC connections to the same host
kckeiks Aug 15, 2022
33f0b33
Merge branch 'master' into add-http3-support
kckeiks Mar 15, 2023
f6ea3b8
Update dns resolver
kckeiks Mar 15, 2023
90fa6e1
Use new DynResolver
kckeiks Mar 15, 2023
8efb857
Reuse dyn Resolver in client
kckeiks Mar 15, 2023
b9e6ea0
Remove unused import
kckeiks Mar 15, 2023
6f5af0e
Run fmt
kckeiks Mar 15, 2023
e233841
Update deps
kckeiks Mar 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
target
Cargo.lock
*.swp
.idea
10 changes: 10 additions & 0 deletions Cargo.toml
Expand Up @@ -62,6 +62,9 @@ stream = ["tokio/fs", "tokio-util"]

socks = ["tokio-socks"]

# Experimental HTTP/3 client.
http3 = ["rustls-tls"]

# Internal (PRIVATE!) features used to aid testing.
# Don't rely on these whatsoever. They may disappear at anytime.

Expand Down Expand Up @@ -136,6 +139,13 @@ tokio-socks = { version = "0.5.1", optional = true }
## trust-dns
trust-dns-resolver = { version = "0.21", optional = true }

# HTTP/3 experimental support
h3 = { git = "https://github.com/hyperium/h3" }
seanmonstar marked this conversation as resolved.
Show resolved Hide resolved
h3-quinn = { git = "https://github.com/hyperium/h3" }
quinn = { version = "0.8", default-features = false, features = ["tls-rustls", "ring"] }
futures-channel = "0.3"


[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
env_logger = "0.8"
hyper = { version = "0.14", default-features = false, features = ["tcp", "stream", "http1", "http2", "client", "server", "runtime"] }
Expand Down
51 changes: 51 additions & 0 deletions examples/h3_simple.rs
@@ -0,0 +1,51 @@
#![deny(warnings)]

// This is using the `tokio` runtime. You'll need the following dependency:
//
// `tokio = { version = "1", features = ["full"] }`
#[cfg(feature = "http3")]
#[cfg(not(target_arch = "wasm32"))]
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
use http::Version;
use reqwest::{Client, IntoUrl, Response};

async fn get<T: IntoUrl + Clone>(url: T) -> reqwest::Result<Response> {
Client::builder()
.http3_prior_knowledge()
.build()?
.get(url)
.version(Version::HTTP_3)
.send()
.await
}

// Some simple CLI args requirements...
let url = match std::env::args().nth(1) {
Some(url) => url,
None => {
println!("No CLI URL provided, using default.");
"https://hyper.rs".into()
}
};

eprintln!("Fetching {:?}...", url);

let res = get(url).await?;

eprintln!("Response: {:?} {}", res.version(), res.status());
eprintln!("Headers: {:#?}\n", res.headers());

let body = res.text().await?;

println!("{}", body);

Ok(())
}

// The [cfg(not(target_arch = "wasm32"))] above prevent building the tokio::main function
// for wasm32 target, because tokio isn't compatible with wasm32.
// If you aren't building for wasm32, you don't need that line.
// The two lines below avoid the "'main' function not found" error when building for wasm32 target.
#[cfg(any(target_arch = "wasm32", not(feature = "http3")))]
fn main() {}
118 changes: 94 additions & 24 deletions src/async_impl/client.rs
Expand Up @@ -13,7 +13,7 @@ use http::header::{
};
use http::uri::Scheme;
use http::Uri;
use hyper::client::ResponseFuture;
use hyper::client::ResponseFuture as HyperResponseFuture;
#[cfg(feature = "native-tls-crate")]
use native_tls_crate::TlsConnector;
use pin_project_lite::pin_project;
Expand All @@ -23,7 +23,6 @@ use std::task::{Context, Poll};
use tokio::time::Sleep;

use log::{debug, trace};

use super::decoder::Accepts;
use super::request::{Request, RequestBuilder};
use super::response::Response;
Expand All @@ -41,6 +40,8 @@ use crate::Certificate;
#[cfg(any(feature = "native-tls", feature = "__rustls"))]
use crate::Identity;
use crate::{IntoUrl, Method, Proxy, StatusCode, Url};
#[cfg(feature = "http3")]
use crate::async_impl::h3_client::{H3Builder, H3Client, H3ResponseFuture};

/// An asynchronous `Client` to make Requests with.
///
Expand Down Expand Up @@ -69,6 +70,7 @@ pub struct ClientBuilder {
enum HttpVersionPref {
Http1,
Http2,
Http3,
All,
}

Expand Down Expand Up @@ -121,6 +123,8 @@ struct Config {
error: Option<crate::Error>,
https_only: bool,
dns_overrides: HashMap<String, SocketAddr>,
#[cfg(feature = "http3")]
tls_enable_early_data: bool
}

impl Default for ClientBuilder {
Expand Down Expand Up @@ -188,6 +192,8 @@ impl ClientBuilder {
cookie_store: None,
https_only: false,
dns_overrides: HashMap::new(),
#[cfg(feature = "http3")]
tls_enable_early_data: false
},
}
}
Expand Down Expand Up @@ -222,15 +228,15 @@ impl ClientBuilder {
if config.dns_overrides.is_empty() {
HttpConnector::new_gai()
} else {
HttpConnector::new_gai_with_overrides(config.dns_overrides)
HttpConnector::new_gai_with_overrides(config.dns_overrides.clone())
}
}
#[cfg(feature = "trust-dns")]
true => {
if config.dns_overrides.is_empty() {
HttpConnector::new_trust_dns()?
} else {
HttpConnector::new_trust_dns_with_overrides(config.dns_overrides)?
HttpConnector::new_trust_dns_with_overrides(config.dns_overrides.clone())?
}
}
#[cfg(not(feature = "trust-dns"))]
Expand All @@ -252,6 +258,9 @@ impl ClientBuilder {
HttpVersionPref::Http2 => {
tls.request_alpns(&["h2"]);
}
HttpVersionPref::Http3 => {
unreachable!("HTTP/3 shouldn't be enabled unless the feature is")
seanmonstar marked this conversation as resolved.
Show resolved Hide resolved
},
HttpVersionPref::All => {
tls.request_alpns(&["h2", "http/1.1"]);
}
Expand Down Expand Up @@ -434,11 +443,19 @@ impl ClientBuilder {
HttpVersionPref::Http2 => {
tls.alpn_protocols = vec!["h2".into()];
}
HttpVersionPref::Http3 => {
tls.alpn_protocols = vec!["h3".into()];
}
HttpVersionPref::All => {
tls.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
}
}

#[cfg(feature = "http3")]
{
tls.enable_early_data = config.tls_enable_early_data;
}

Connector::new_rustls_tls(
http,
tls,
Expand Down Expand Up @@ -508,16 +525,25 @@ impl ClientBuilder {
builder.http1_allow_obsolete_multiline_headers_in_responses(true);
}

let hyper_client = builder.build(connector);

let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth());

#[cfg(feature = "http3")]
let h3_builder = {
let mut h3_builder = H3Builder::default();
h3_builder.set_local_addr(config.local_address);
h3_builder.set_pool_idle_timeout(config.pool_idle_timeout);
h3_builder.set_pool_max_idle_per_host(config.pool_max_idle_per_host);
h3_builder
};

Ok(Client {
inner: Arc::new(ClientRef {
accepts: config.accepts,
#[cfg(feature = "cookies")]
cookie_store: config.cookie_store,
hyper: hyper_client,
#[cfg(feature = "http3")]
h3_client: h3_builder.build(connector.deep_clone_tls()),
hyper: builder.build(connector),
headers: config.headers,
redirect_policy: config.redirect_policy,
referer: config.referer,
Expand Down Expand Up @@ -914,6 +940,12 @@ impl ClientBuilder {
self
}

/// Only use HTTP/3.
pub fn http3_prior_knowledge(mut self) -> ClientBuilder {
self.config.http_version_pref = HttpVersionPref::Http3;
self
}

/// Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control.
///
/// Default is currently 65,535 but may change internally to optimize for common uses.
Expand Down Expand Up @@ -1492,22 +1524,30 @@ impl Client {

self.proxy_auth(&uri, &mut headers);

let mut req = hyper::Request::builder()
let builder = hyper::Request::builder()
.method(method.clone())
.uri(uri)
.version(version)
.body(body.into_stream())
.expect("valid request parts");
.version(version);

let in_flight = match version {
#[cfg(feature = "http3")]
http::Version::HTTP_3 => {
let mut req = builder.body(()).expect("valid request parts");
*req.headers_mut() = headers.clone();
ResponseFuture::H3(self.inner.h3_client.request(req))
},
_ => {
let mut req = builder.body(body.into_stream()).expect("valid request parts");
*req.headers_mut() = headers.clone();
ResponseFuture::Default(self.inner.hyper.request(req))
},
};

let timeout = timeout
.or(self.inner.request_timeout)
.map(tokio::time::sleep)
.map(Box::pin);

*req.headers_mut() = headers.clone();

let in_flight = self.inner.hyper.request(req);

Pending {
inner: PendingInner::Request(PendingRequest {
method,
Expand Down Expand Up @@ -1689,6 +1729,13 @@ impl Config {
if !self.dns_overrides.is_empty() {
f.field("dns_overrides", &self.dns_overrides);
}

#[cfg(feature = "http3")]
{
if self.tls_enable_early_data {
f.field("tls_enable_early_data", &true);
}
}
}
}

Expand All @@ -1698,6 +1745,8 @@ struct ClientRef {
cookie_store: Option<Arc<dyn cookie::CookieStore>>,
headers: HeaderMap,
hyper: HyperClient,
#[cfg(feature = "http3")]
h3_client: H3Client,
redirect_policy: redirect::Policy,
referer: bool,
request_timeout: Option<Duration>,
Expand Down Expand Up @@ -1772,6 +1821,12 @@ pin_project! {
}
}

enum ResponseFuture {
Default(HyperResponseFuture),
#[cfg(feature = "http3")]
H3(H3ResponseFuture),
}

impl PendingRequest {
fn in_flight(self: Pin<&mut Self>) -> Pin<&mut ResponseFuture> {
self.project().in_flight
Expand Down Expand Up @@ -1820,7 +1875,7 @@ impl PendingRequest {

*req.headers_mut() = self.headers.clone();

*self.as_mut().in_flight().get_mut() = self.client.hyper.request(req);
*self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req));

true
}
Expand Down Expand Up @@ -1877,15 +1932,30 @@ impl Future for PendingRequest {
}

loop {
let res = match self.as_mut().in_flight().as_mut().poll(cx) {
Poll::Ready(Err(e)) => {
if self.as_mut().retry_error(&e) {
continue;
let res = match self.as_mut().in_flight().get_mut() {
ResponseFuture::Default(r) => {
match Pin::new(r).poll(cx) {
Poll::Ready(Err(e)) => {
if self.as_mut().retry_error(&e) {
continue;
}
return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone())));
}
Poll::Ready(Ok(res)) => res,
Poll::Pending => return Poll::Pending,
}
}
#[cfg(feature = "http3")]
ResponseFuture::H3(r) => match Pin::new(r).poll(cx) {
Poll::Ready(Err(e)) => {
if self.as_mut().retry_error(&e) {
continue;
}
return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone())));
}
return Poll::Ready(Err(crate::error::request(e).with_url(self.url.clone())));
Poll::Ready(Ok(res)) => res,
Poll::Pending => return Poll::Pending,
}
Poll::Ready(Ok(res)) => res,
Poll::Pending => return Poll::Pending,
};

#[cfg(feature = "cookies")]
Expand Down Expand Up @@ -2001,7 +2071,7 @@ impl Future for PendingRequest {

*req.headers_mut() = headers.clone();
std::mem::swap(self.as_mut().headers(), &mut headers);
*self.as_mut().in_flight().get_mut() = self.client.hyper.request(req);
*self.as_mut().in_flight().get_mut() = ResponseFuture::Default(self.client.hyper.request(req));
continue;
}
redirect::ActionKind::Stop => {
Expand Down