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
- Verify the upstream peer type matches the client protocol path: only route H2 downstream sessions (or properly converted requests) through `proxy_to_h2_upstream`.
- 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.
- If writing custom code, convert the downstream request into the H2 request type before calling this API rather than passing the raw H1 request.
- 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
- Match upstream peer protocol (h1 vs h2) to the downstream session type in configuration.
- Don't reuse H1 request objects in H2 upstream code paths; convert first.
- Keep custom `upstream_request` filters type-neutral so they don't strip the H2 wrapper.
- When adding new proxy paths, assert the session/request variant before entering the H2 code.
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
- non-pathname unix sockets not supported as peer
- Tried to listen with no addr specified
- non-pathname unix sockets not supported as peer
- failed to build tokio runtime for parent signal wait
- failed to register SIGUSR1 listener
AI-assisted analysis of cloudflare/pingora@4487f7b2ab (2026-09-13).
Data as JSON: /api/errors/489c59e166d362ee.
Report an issue: GitHub.