cloudflare/pingora · error · panic

req must be h2

Error message

req must be h2

What it means

In `proxy_down_to_up` (pingora-proxy/src/proxy_h2.rs), `req.send_end_stream()` is an API that only exists on the H2 downstream request representation; the code calls `.expect("req must be h2")` asserting the downstream session is an HTTP/2 session when proxying to an H2 upstream. The expect fails — panicking with "req must be h2" — when this H2-upstream proxy path is reached with a request that is not the H2 type (e.g., an H1 downstream request converted or reused into this path).

Source

Thrown at pingora-proxy/src/proxy_h2.rs:234

        // `http::request::Parts` discards RequestHeader's raw byte fallback. A failure here after
        // the initial check was produced by a filter and is therefore internal.
        let path_and_query = match h2_path_and_query(&req) {
            Ok(path_and_query) => path_and_query,
            Err(e) => return (false, Some(e.into_in())),
        };

        // Remove H1 `Host` header, save it in order to add to :authority
        // We do this because certain H2 servers expect request not to have a host header.
        // The `Host` is removed after the upstream filters above for 2 reasons
        // 1. there is no API to change the :authority header
        // 2. the filter code needs to be aware of the host vs :authority across http versions otherwise
        let host = req.remove_header(&http::header::HOST);

        session.upstream_compression.request_filter(&req);
        let body_empty = session.as_mut().is_body_empty();

        // whether we support sending END_STREAM on HEADERS if body is empty
        let send_end_stream = req.send_end_stream().expect("req must be h2");

        // Host is consumed locally to build :authority and is never sent on the H2 wire.
        let authority = host
            .as_ref()
            .map(|host| host.as_bytes())
            .or(raw_authority.as_deref());
        if let Some(authority) = authority {
            if let Err(e) =
                update_h2_scheme_authority(&mut req, authority, peer.is_tls(), path_and_query)
            {
                return (false, Some(e));
            }
        }

        let req: http::request::Parts = req.into();

        debug!("Request to h2: {req:?}");

View on GitHub (pinned to 4487f7b2ab)

Solutions

  1. Verify the upstream peer type matches the client protocol path: only route H2 downstream sessions (or properly converted requests) through `proxy_to_h2_upstream`.
  2. Ensure `http_upgrade`/listener configuration creates H2 sessions for H2 upstreams, or configure the upstream as h1 so the h1 proxy path is used instead.
  3. If writing custom code, convert the downstream request into the H2 request type before calling this API rather than passing the raw H1 request.
  4. If this occurs with a stock setup, report/inspect the version: this is an internal type invariant; confirm the downgrade/conversion step in the session handoff ran.

Example fix

// before
let send_end_stream = req.send_end_stream().expect("req must be h2");
// after (caller side: pick the right proxy path for the session type)
match session.req_version() {
    http::Version::HTTP_2 => proxy_to_h2_upstream(session).await,
    _ => proxy_to_h1_upstream(session).await, // don't feed h1 reqs to the h2 path
}
Defensive patterns

Strategy: validation

Validate before calling

// Route by downstream protocol version before choosing the h2 upstream path
fn use_h2_upstream_path(req_version: http::Version) -> bool {
    req_version == http::Version::HTTP_2
}

Type guard

fn as_h2_req(req: &Session) -> Option<&pingora_h2::server::Request> {
    match req {
        // narrow to the h2 request variant; None for h1/custom sessions
        _ => None, // fill in with your session enum's H2 variant
    }
}

Prevention

When it happens

Trigger: Reaching `proxy_down_to_up` (called from `proxy_to_h2_upstream`) with a downstream session whose request is not the H2 request type — i.e., proxying an HTTP/1.1 downstream request to an H2 upstream through a code path that bypasses the proper H1→H2 request conversion.

Common situations: Misconfigured listener/upstream combination where an HTTP/1.x client connection is routed to an upstream declared as H2; custom proxy logic (e.g., in `upstream_request` filters) that mutates or replaces the request in a way that loses the H2 wrapper; internal regression after upgrading where the session type check before this point was removed.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of cloudflare/pingora@4487f7b2ab (2026-09-13). Data as JSON: /api/errors/489c59e166d362ee. Report an issue: GitHub.