HTTP/3 request fails with http3_prior_knowledge
HTTP/3 request fails with http3_prior_knowledge
test repo https://github.com/i18n-site/gway
Description
When attempting to make an HTTP/3 request using reqwest with http3_prior_knowledge, the request fails. However, making a direct HTTP/3 request to the same test server using the s2n-quic client works successfully. This suggests a potential issue with reqwest's HTTP/3 implementation.
I have two tests to demonstrate this:
h3_reqwest: This test usesreqwestand it fails (cargo test -F cert_dir --test h3_reqwest).h3: This test usess2n-quicdirectly and it passes (cargo test -F cert_dir --test h3).
Code
Here is the relevant code for the tests.
tests/h3_reqwest.rs (The failing test using reqwest)
mod gway_srv;
mod util;
use std::net::SocketAddr;
use gway_srv::{TEST_HOST, TEST_RESPONSE_BODY};
use tokio::time::{Duration, sleep};
#[tokio::test]
async fn test_h3_proxy() -> anyhow::Result<()> {
// sleep 1s to wait for the server to start
sleep(Duration::from_secs(1)).await;
let path = "/";
let h3_addr: SocketAddr = gway_srv::H3_ADDR.parse()?;
let url = format!("https://{TEST_HOST}{path}");
let body = util::get_body_h3(&url, h3_addr).await?;
println!("h3 body: {}", body);
assert_eq!(body, TEST_RESPONSE_BODY);
println!("h3 proxy test passed");
Ok(())
}
error
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running tests/h3_reqwest.rs (/tmp/rust/target/debug/deps/h3_reqwest-8004286d30e8a95d)
running 1 test
h1 127.0.0.1:9081
h2 127.0.0.1:9082
h3 127.0.0.1:9083
Upstream server started on 127.0.0.1:9080
DEBUG quinn_udp::imp: /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/quinn-udp-0.5.13/src/unix.rs:121: Ignoring error setting IP_RECVTOS on socket: Os { code: 22, kind: InvalidInput, message: "Invalid argument" }
DEBUG reqwest::connect: /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/reqwest-0.12.23/src/connect.rs:882: starting new connection: https://018007.xyz/
DEBUG hyper_util::client::legacy::connect::http: /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/hyper-util-0.1.16/src/client/legacy/connect/http.rs:768: connecting to 127.0.0.1:9083
test test_h3_proxy ... FAILED
failures:
---- test_h3_proxy stdout ----
Attempting to connect to https://018007.xyz/ -> 127.0.0.1:9083
Error: error sending request for url (https://018007.xyz/)
Caused by:
0: client error (Connect)
1: tcp connect error
2: Connection refused (os error 61)
Stack backtrace:
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
1: std::backtrace_rs::backtrace::trace_unsynchronized
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
2: std::backtrace::Backtrace::create
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/backtrace.rs:331:13
3: anyhow::error::<impl core::convert::From<E> for anyhow::Error>::from
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/anyhow-1.0.99/src/backtrace.rs:27:14
4: <T as core::convert::Into<U>>::into
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/convert/mod.rs:784:9
5: core::ops::function::FnOnce::call_once
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
6: core::result::Result<T,E>::map_err
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:960:27
7: h3_reqwest::util::get_with_builder::{{closure}}
at ./tests/util.rs:26:36
8: h3_reqwest::util::get_body_h3::{{closure}}
at ./tests/util.rs:48:76
9: h3_reqwest::test_h3_proxy::{{closure}}
at ./tests/h3_reqwest.rs:16:47
10: <core::pin::Pin<P> as core::future::future::Future>::poll
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/future/future.rs:133:9
11: <core::pin::Pin<P> as core::future::future::Future>::poll
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/future/future.rs:133:9
12: tokio::runtime::scheduler::current_thread::CoreGuard::block_on::{{closure}}::{{closure}}::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:742:70
13: tokio::task::coop::with_budget
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/task/coop/mod.rs:167:5
14: tokio::task::coop::budget
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/task/coop/mod.rs:133:5
15: tokio::runtime::scheduler::current_thread::CoreGuard::block_on::{{closure}}::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:742:25
16: tokio::runtime::scheduler::current_thread::Context::enter
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:432:19
17: tokio::runtime::scheduler::current_thread::CoreGuard::block_on::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:741:44
18: tokio::runtime::scheduler::current_thread::CoreGuard::enter::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:829:68
19: tokio::runtime::context::scoped::Scoped<T>::set
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/context/scoped.rs:40:9
20: tokio::runtime::context::set_scheduler::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/context.rs:176:38
21: std::thread::local::LocalKey<T>::try_with
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/local.rs:315:12
22: std::thread::local::LocalKey<T>::with
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/local.rs:279:20
23: tokio::runtime::context::set_scheduler
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/context.rs:176:17
24: tokio::runtime::scheduler::current_thread::CoreGuard::enter
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:829:27
25: tokio::runtime::scheduler::current_thread::CoreGuard::block_on
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:729:24
26: tokio::runtime::scheduler::current_thread::CurrentThread::block_on::{{closure}}
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:200:33
27: tokio::runtime::context::runtime::enter_runtime
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/context/runtime.rs:65:16
28: tokio::runtime::scheduler::current_thread::CurrentThread::block_on
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/scheduler/current_thread/mod.rs:188:9
29: tokio::runtime::runtime::Runtime::block_on_inner
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/runtime.rs:356:52
30: tokio::runtime::runtime::Runtime::block_on
at /Users/z/.cargo/registry/src/github.com-25cdd57fae9f0462/tokio-1.47.1/src/runtime/runtime.rs:330:18
31: h3_reqwest::test_h3_proxy
at ./tests/h3_reqwest.rs:23:5
32: h3_reqwest::test_h3_proxy::{{closure}}
at ./tests/h3_reqwest.rs:9:29
33: core::ops::function::FnOnce::call_once
at /Users/z/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
34: core::ops::function::FnOnce::call_once
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/core/src/ops/function.rs:253:5
35: test::__rust_begin_short_backtrace
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/test/src/lib.rs:663:18
36: test::run_test_in_process::{{closure}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/test/src/lib.rs:686:74
37: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/core/src/panic/unwind_safe.rs:272:9
38: std::panicking::catch_unwind::do_call
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panicking.rs:590:40
39: std::panicking::catch_unwind
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panicking.rs:553:19
40: std::panic::catch_unwind
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panic.rs:359:14
41: test::run_test_in_process
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/test/src/lib.rs:686:27
42: test::run_test::{{closure}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/test/src/lib.rs:607:43
43: test::run_test::{{closure}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/test/src/lib.rs:637:41
44: std::sys::backtrace::__rust_begin_short_backtrace
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/sys/backtrace.rs:158:18
45: std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/thread/mod.rs:559:17
46: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/core/src/panic/unwind_safe.rs:272:9
47: std::panicking::catch_unwind::do_call
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panicking.rs:590:40
48: std::panicking::catch_unwind
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panicking.rs:553:19
49: std::panic::catch_unwind
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/panic.rs:359:14
50: std::thread::Builder::spawn_unchecked_::{{closure}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/thread/mod.rs:557:30
51: core::ops::function::FnOnce::call_once{{vtable.shim}}
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/core/src/ops/function.rs:253:5
52: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/alloc/src/boxed.rs:1985:9
53: std::sys::pal::unix::thread::Thread::new::thread_start
at /rustc/6ba0ce40941eee1ca02e9ba49c791ada5158747a/library/std/src/sys/pal/unix/thread.rs:118:17
54: __pthread_cond_wait
failures:
test_h3_proxy
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.22s
error: test failed, to rerun pass `--test h3_reqwest`
tests/h3.rs (The successful test using s2n-quic)
mod gway_srv;
mod util;
use std::{net::SocketAddr, time::Duration};
use anyhow::Result;
use bytes::Buf;
use gway_srv::{TEST_HOST, TEST_RESPONSE_BODY};
use http::{Method, Request, Uri};
use s2n_quic::Client;
use tokio::time::sleep;
/// 使用 s2n-quic 客户端发送 H3 请求
async fn send_h3_request(
addr: SocketAddr,
host: &str,
path: &str,
) -> Result<(http::StatusCode, http::HeaderMap, String)> {
// 等待服务器启动
sleep(Duration::from_secs(1)).await;
// 创建 s2n-quic 客户端
let tls = s2n_quic::provider::tls::s2n_tls::Client::builder().build()?;
let client = Client::builder()
.with_tls(tls)?
.with_io("0.0.0.0:0")?
.start()?;
// 连接到服务器
let connect_to = s2n_quic::client::Connect::new(addr).with_server_name(TEST_HOST);
let mut connection = client.connect(connect_to).await?;
// 等待连接建立
connection.keep_alive(true)?;
// 创建 H3 连接
let h3_conn = gway::srv::s2n_quic::Connection::new(connection);
let (_driver, mut send_request) = h3::client::new(h3_conn).await?;
let uri: Uri = format!("https://{}{}", host, path).parse()?;
let req = Request::builder()
.method(Method::GET)
.uri(uri)
.header("host", host)
.body(())?;
let mut stream = send_request.send_request(req).await?;
stream.finish().await?;
let resp = stream.recv_response().await?;
println!("H3 Response status: {}", resp.status());
let status = resp.status();
let headers = resp.headers().clone();
let mut body = Vec::new();
while let Some(mut chunk) = stream.recv_data().await? {
let chunk_bytes = chunk.copy_to_bytes(chunk.remaining());
body.extend_from_slice(&chunk_bytes);
}
Ok((status, headers, String::from_utf8(body)?))
}
#[tokio::test]
async fn test_h3_s2n_client() -> Result<()> {
let h3_addr: SocketAddr = gway_srv::H3_ADDR.parse()?;
let path = "/";
println!("Testing with s2n-quic H3 client...");
// 发送请求
let (_status, _headers, body) = send_h3_request(h3_addr, TEST_HOST, path).await?;
println!("H3 response body: {}", body);
assert_eq!(body, TEST_RESPONSE_BODY);
println!("s2n-quic H3 client test passed!");
Ok(())
}
#[tokio::test]
async fn h3_test_with_h3_client() -> Result<()> {
let h3_addr: SocketAddr = gway_srv::H3_ADDR.parse()?;
let path = "/";
println!("Testing with h3-quinn H3 client...");
// 发送请求
let body = send_h3_request(h3_addr, TEST_HOST, path).await?.2;
println!("H3 response body: {}", body);
assert_eq!(body, TEST_RESPONSE_BODY);
println!("h3-quinn H3 client test passed!");
Ok(())
}
tests/util.rs (Helper functions for reqwest)
#![allow(dead_code)]
use std::net::SocketAddr;
use reqwest::{ClientBuilder, Response};
pub async fn get_with_builder(
url_str: &str,
addr: SocketAddr,
builder: impl FnOnce(ClientBuilder) -> ClientBuilder,
) -> anyhow::Result<Response> {
let url = url::Url::parse(url_str)?;
let host = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("URL does not have a host"))?;
let client_builder = builder(reqwest::Client::builder()).use_rustls_tls();
let client = client_builder
.redirect(reqwest::redirect::Policy::none())
.resolve(host, addr)
.no_proxy()
.build()?;
println!("Attempting to connect to {} -> {}", url_str, addr);
client.get(url_str).send().await.map_err(Into::into)
}
pub async fn get(url_str: &str, addr: SocketAddr) -> anyhow::Result<Response> {
get_with_builder(url_str, addr, |c| c).await
}
pub async fn get_body(url_str: &str, addr: SocketAddr) -> anyhow::Result<String> {
let res = get(url_str, addr).await?;
res.text().await.map_err(Into::into)
}
pub async fn get_body_h2(url_str: &str, addr: SocketAddr) -> anyhow::Result<String> {
let res = get_with_builder(url_str, addr, |c| c.http2_prior_knowledge()).await?;
res.text().await.map_err(Into::into)
}
pub async fn get_response_h2(url_str: &str, addr: SocketAddr) -> anyhow::Result<Response> {
get_with_builder(url_str, addr, |c| c.http2_prior_knowledge()).await
}
pub async fn get_body_h3(url_str: &str, addr: SocketAddr) -> anyhow::Result<String> {
let res = get_with_builder(url_str, addr, |c| c.http3_prior_knowledge()).await?;
res.text().await.map_err(Into::into)
}
pub async fn get_response_h3(url_str: &str, addr: SocketAddr) -> anyhow::Result<Response> {
get_with_builder(url_str, addr, |c| c.http3_prior_knowledge()).await
}
Expected Behavior
The h3_reqwest test should pass, and the client should successfully receive the response body from the server over HTTP/3.
Actual Behavior
The h3_reqwest test fails. The request does not complete successfully.