seanmonstar / reqwest

An easy and powerful Rust HTTP Client
https://docs.rs/reqwest
Apache License 2.0
9.5k stars 1.07k forks source link

HTTP/2 not working #2348

Closed ColonelThirtyTwo closed 2 weeks ago

ColonelThirtyTwo commented 2 weeks ago

Requesting HTTP/2 on a request does not work:

Example code:

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let client = reqwest::ClientBuilder::new()
        .build()
        .unwrap();

    let req = client.get("https://http2.pro/api/v1")
        .version(reqwest::Version::HTTP_2)
        .send()
        .await
        .unwrap();
    let body = req.text().await.unwrap();
    println!("{}", body);
}

Cargo.toml:

[package]
name = "reqwest-http2-test"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.38.0", features = ["full", "macros"] }
reqwest = { version = "0.12.5", features = ["native-tls"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing = "0.1.40"

Output:

2024-07-09T13:46:12.068025Z DEBUG reqwest::connect: starting new connection: https://http2.pro/    
2024-07-09T13:46:12.068226Z DEBUG hyper_util::client::legacy::connect::dns: resolving host="http2.pro"
2024-07-09T13:46:12.069833Z DEBUG hyper_util::client::legacy::connect::http: connecting to [2001:19f0:5:3cdd::]:443
2024-07-09T13:46:12.105917Z DEBUG hyper_util::client::legacy::connect::http: connected to [2001:19f0:5:3cdd::]:443
2024-07-09T13:46:12.183523Z DEBUG hyper_util::client::legacy::pool: pooling idle connection for ("https", http2.pro)
{"http2":0,"protocol":"HTTP\/1.1","push":0,"user_agent":""}
2024-07-09T13:46:12.183667Z DEBUG hyper_util::client::legacy::pool: reuse idle connection for ("https", http2.pro)
2024-07-09T13:46:12.183693Z  WARN hyper_util::client::legacy::client: Connection is HTTP/1, but request requires HTTP/2
2024-07-09T13:46:12.183705Z DEBUG hyper_util::client::legacy::pool: pooling idle connection for ("https", http2.pro)
thread 'main' panicked at src/main.rs:20:10:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("http2.pro")), port: None, path: "/api/v1", query: None, fragment: None }, source: hyper_util::client::legacy::Error(UserUnsupportedVersion) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

In my app I'm developing, the warning "Connection is HTTP/1, but request requires HTTP/2" emits but the request goes through anyway using HTTP/1.1, which is causing issues because unfortunately the site I'm connecting to has different behavior when connecting with HTTP/2.

Adding http2_prior_knowledge() to the builder causes a different error (and that API is annoying because it applies to the entire client):

2024-07-09T13:55:13.295777Z DEBUG reqwest::connect: starting new connection: https://http2.pro/    
2024-07-09T13:55:13.295971Z DEBUG hyper_util::client::legacy::connect::dns: resolving host="http2.pro"
2024-07-09T13:55:13.297830Z DEBUG hyper_util::client::legacy::connect::http: connecting to [2001:19f0:5:3cdd::]:443
2024-07-09T13:55:13.333432Z DEBUG hyper_util::client::legacy::connect::http: connected to [2001:19f0:5:3cdd::]:443
2024-07-09T13:55:13.375803Z DEBUG h2::client: binding client connection
2024-07-09T13:55:13.375851Z DEBUG h2::client: client connection bound
2024-07-09T13:55:13.375875Z DEBUG h2::codec::framed_write: send frame=Settings { flags: (0x0), enable_push: 0, initial_window_size: 2097152, max_frame_size: 16384, max_header_list_size: 16384 }
2024-07-09T13:55:13.376035Z DEBUG hyper_util::client::legacy::pool: pooling idle connection for ("https", http2.pro)
2024-07-09T13:55:13.376163Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=WindowUpdate { stream_id: StreamId(0), size_increment: 5177345 }
2024-07-09T13:55:13.376215Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=Headers { stream_id: StreamId(1), flags: (0x5: END_HEADERS | END_STREAM) }
2024-07-09T13:55:13.410797Z DEBUG Connection{peer=Client}: h2::proto::connection: Connection::poll; connection error error=GoAway(b"", FRAME_SIZE_ERROR, Library)
2024-07-09T13:55:13.410946Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=GoAway { error_code: FRAME_SIZE_ERROR, last_stream_id: StreamId(0) }
2024-07-09T13:55:13.410999Z DEBUG Connection{peer=Client}: h2::proto::connection: Connection::poll; connection error error=GoAway(b"", FRAME_SIZE_ERROR, Library)
thread 'main' panicked at src/main.rs:14:10:
called `Result::unwrap()` on an `Err` value: reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("http2.pro")), port: None, path: "/api/v1", query: None, fragment: None }, source: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Http2, Error { kind: GoAway(b"", FRAME_SIZE_ERROR, Library) })) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2024-07-09T13:55:13.411309Z DEBUG hyper_util::client::legacy::client: client connection error: http2 error

The same error happens with https://www.google.com, so I don't think it's an issue with the particular server.

Requests without http2_prior_knowledge or version don't seem to be upgraded to HTTP/2 at any point either.

seanmonstar commented 2 weeks ago

Due to how HTTP/2 works with most servers, you need to enable a TLS feature which will use ALPN. That's either native-tls-alpn, or rustls-tls (and make sure your client builds with use_rustls_tls()). It's not enabled by default for native-tls because not all machines will have the necessary support (rustls does out of the box).

ColonelThirtyTwo commented 2 weeks ago

Adding features = ["native-tls-alpn"] makes it work, thank you.

Seems like a footgun though - the native-tls-alpn feature on reqwests just mentions the corresponding feature on the native-tls crate, which itself doesn't have any documentation. I'd be nice if either the http2 feature or native-tls had a note mentioning you need native-tls-alpn for http/2 to work.

KUAILEJIANLI commented 2 weeks ago

Due to how HTTP/2 works with most servers, you need to enable a TLS feature which will use ALPN. That's either native-tls-alpn, or rustls-tls (and make sure your client builds with use_rustls_tls()). It's not enabled by default for native-tls because not all machines will have the necessary support (rustls does out of the box).

Does reqwest automatically reuse http2 connections? For example, I want to send multiple requests to the same URL.


    let url = "https:://example.com";
    let mut handles = vec![];
    for i in 0..10 {
        let client = client.clone();
        let url = url.to_string();
        let handle = task::spawn(async move {
            let resp = client.get(&url).send().await?;
            let text = resp.text().await?;
            // println!("Response {}: {}", i + 1, text);
            Ok::<(), reqwest::Error>(())
        });
        handles.push(handle);
    }```