Skip to content

HTTP2 CONNECT request with body creates undescriptive INTERNAL_ERROR error #3858

Closed
@manuel2258

Description

@manuel2258

Version

hyper: v1.6.0
hyper-tls: v0.6.0
hyper-util: v0.1.10
h2: v0.4.8
reqwest: v0.12.15

Platform

Linux workfedora 6.13.6-100.fc40.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Mar 7 21:23:12 UTC 2025 x86_64 GNU/Linux

Description

Hey, we recently stumbled across a weird undescriptive INTERNAL_ERROR hyper error when using reqwest.

We are writing a pentesting tool, and as a part of that we wanted to test whether a HTTP2 endpoint rejects invalid methods. As a part of that, we sent a CONNECT request, with a body.
That resulted in the error: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Http2, Error { kind: Reason(INTERNAL_ERROR) })) }

It can be reproduced with:

use reqwest::Method;
use tracing::Level;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    tracing_subscriber::fmt()
        .with_max_level(Level::TRACE)
        .init();

    let client = reqwest::Client::builder()
        .http2_prior_knowledge()
        .build()
        .unwrap();

    let res = client
        .request(Method::CONNECT, "https://nghttp2.org/httpbin/post")
        .body("example body")
        .send()
        .await;
    println!("{res:?}");

    Ok(())
}

Doing a bit of investigation with the backtrace crate, I gathered that the error originates from h2/client.rs.

Two weird things about that:

  1. We did not see the tracing warn message, so it was really hard to figure out what is going wrong. Not sure whether this is a configuration error on our side, but we see all the other tracing events from hyper.

Log from example:

2025-03-20T08:53:10.774367Z DEBUG reqwest::connect: starting new connection: https://nghttp2.org/
2025-03-20T08:53:10.775902Z DEBUG hyper_util::client::legacy::connect::http: connecting to [2400:8902::f03c:91ff:fe69:a454]:443
2025-03-20T08:53:11.077096Z DEBUG hyper_util::client::legacy::connect::http: connecting to 139.162.123.134:443
2025-03-20T08:53:11.344664Z DEBUG hyper_util::client::legacy::connect::http: connected to 139.162.123.134:443
2025-03-20T08:53:11.702388Z DEBUG h2::client: binding client connection
2025-03-20T08:53:11.702435Z DEBUG h2::client: client connection bound
2025-03-20T08:53:11.702457Z 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 }
2025-03-20T08:53:11.702595Z DEBUG hyper_util::client::legacy::pool: pooling idle connection for ("https", nghttp2.org)
2025-03-20T08:53:11.702641Z  WARN hyper_util::client::legacy::client: HTTP/1.1 CONNECT request stripping path: /httpbin/post
Err(reqwest::Error { kind: Request, url: "https://nghttp2.org/httpbin/post", source: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Http2, Error { kind: Reason(INTERNAL_ERROR) })) })
2025-03-20T08:53:11.702817Z DEBUG Connection{peer=Client}: h2::codec::framed_write: send frame=WindowUpdate { stream_id: StreamId(0), size_increment: 5177345 }
  1. Somehow, this path is only triggered when using HTTP2 with TLS. Using HTTP2 without TLS does not trigger the error. I know that that is then rather on how reqwest is using hyper, but still, maybe there could be a better place to check on whether the body is empty?

Not sure whether there is anything to fix / change here, we primarly wanted to report this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-clientArea: client.A-http2Area: HTTP/2 specific.C-refactorCategory: refactor. This would improve the clarity of internal code.E-easyEffort: easy. A task that would be a great starting point for a new contributor.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions