reqwest icon indicating copy to clipboard operation
reqwest copied to clipboard

HTTP/3 request fails with http3_prior_knowledge

Open i18nsite opened this issue 3 months ago • 8 comments

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 uses reqwest and it fails (cargo test -F cert_dir --test h3_reqwest).
  • h3: This test uses s2n-quic directly 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.

i18nsite avatar Aug 27 '25 08:08 i18nsite