{"record":{"id":"489c59e166d362ee","repo":"cloudflare/pingora","slug":"req-must-be-h2","errorCode":null,"errorMessage":"req must be h2","messagePattern":"req must be h2","errorType":"panic","errorClass":"panic","httpStatus":null,"severity":"error","filePath":"pingora-proxy/src/proxy_h2.rs","lineNumber":234,"sourceCode":"        // `http::request::Parts` discards RequestHeader's raw byte fallback. A failure here after\n        // the initial check was produced by a filter and is therefore internal.\n        let path_and_query = match h2_path_and_query(&req) {\n            Ok(path_and_query) => path_and_query,\n            Err(e) => return (false, Some(e.into_in())),\n        };\n\n        // Remove H1 `Host` header, save it in order to add to :authority\n        // We do this because certain H2 servers expect request not to have a host header.\n        // The `Host` is removed after the upstream filters above for 2 reasons\n        // 1. there is no API to change the :authority header\n        // 2. the filter code needs to be aware of the host vs :authority across http versions otherwise\n        let host = req.remove_header(&http::header::HOST);\n\n        session.upstream_compression.request_filter(&req);\n        let body_empty = session.as_mut().is_body_empty();\n\n        // whether we support sending END_STREAM on HEADERS if body is empty\n        let send_end_stream = req.send_end_stream().expect(\"req must be h2\");\n\n        // Host is consumed locally to build :authority and is never sent on the H2 wire.\n        let authority = host\n            .as_ref()\n            .map(|host| host.as_bytes())\n            .or(raw_authority.as_deref());\n        if let Some(authority) = authority {\n            if let Err(e) =\n                update_h2_scheme_authority(&mut req, authority, peer.is_tls(), path_and_query)\n            {\n                return (false, Some(e));\n            }\n        }\n\n        let req: http::request::Parts = req.into();\n\n        debug!(\"Request to h2: {req:?}\");\n","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/cloudflare/pingora/blob/4487f7b2ab50f159e4a2cf4f6a6b813f61bb6e19/pingora-proxy/src/proxy_h2.rs#L216-L252","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet send_end_stream = req.send_end_stream().expect(\"req must be h2\");\n// after (caller side: pick the right proxy path for the session type)\nmatch session.req_version() {\n    http::Version::HTTP_2 => proxy_to_h2_upstream(session).await,\n    _ => proxy_to_h1_upstream(session).await, // don't feed h1 reqs to the h2 path\n}","handlingStrategy":"validation","validationCode":"// Route by downstream protocol version before choosing the h2 upstream path\nfn use_h2_upstream_path(req_version: http::Version) -> bool {\n    req_version == http::Version::HTTP_2\n}","typeGuard":"fn as_h2_req(req: &Session) -> Option<&pingora_h2::server::Request> {\n    match req {\n        // narrow to the h2 request variant; None for h1/custom sessions\n        _ => None, // fill in with your session enum's H2 variant\n    }\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["http2","panic","type-mismatch","proxy"],"backgroundTag":"type-mismatch","analyzedSha":"4487f7b2ab50f159e4a2cf4f6a6b813f61bb6e19","analyzedAt":"2026-09-13T07:48:52.011Z","contentChangedAt":"2026-09-13T07:48:52.011Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}