From e00b17a2fc6df29faf533caccf2490d01b0ae1c1 Mon Sep 17 00:00:00 2001 From: gngpp Date: Sat, 12 Sep 2026 23:28:08 +0800 Subject: [PATCH 1/2] fix(client): reject responses without `:status` --- src/client.rs | 5 +++ tests/h2-tests/tests/client_request.rs | 58 ++++++++++++++++++++++++++ tests/h2-tests/tests/stream_states.rs | 2 +- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index a1a9a7816..39c5765bf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1727,6 +1727,11 @@ impl proto::Peer for Peer { if let Some(status) = pseudo.status { b = b.status(status); + } else { + // Every response must include :status (RFC 9113, section 8.3.2). + // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.2 + proto_err!(stream: "missing :status; stream={:?}", stream_id); + return Err(Error::library_reset(stream_id, Reason::PROTOCOL_ERROR)); } let mut response = match b.body(()) { diff --git a/tests/h2-tests/tests/client_request.rs b/tests/h2-tests/tests/client_request.rs index 04f8dddaf..2ade37a66 100644 --- a/tests/h2-tests/tests/client_request.rs +++ b/tests/h2-tests/tests/client_request.rs @@ -820,6 +820,64 @@ async fn sending_request_on_closed_connection() { join(srv, h2).await; } +#[tokio::test] +async fn response_missing_status() { + h2_support::trace_init!(); + let (io, mut srv) = mock::new(); + + let srv = async move { + srv.assert_client_handshake().await; + srv.recv_frame( + frames::headers(1) + .request("GET", "https://example.com/") + .eos(), + ) + .await; + + srv.send_frame(frames::headers(1).eos()).await; + + srv.recv_frame( + frames::headers(3) + .request("GET", "https://example.com/") + .eos(), + ) + .await; + + srv.send_frame(frames::headers(3).response(103)).await; + srv.send_frame(frames::headers(3).field("server", "test")) + .await; + + srv.recv_frame(frames::reset(3).protocol_error()).await; + srv.recv_frame( + frames::headers(5) + .request("GET", "https://example.com/") + .eos(), + ) + .await; + + srv.send_frame(frames::headers(5).response(204).eos()).await; + }; + + let client = async move { + let (mut client, mut conn) = client::handshake(io).await.unwrap(); + for _ in 0..2 { + let request = Request::get("https://example.com/").body(()).unwrap(); + let (response, _) = client.send_request(request, true).unwrap(); + let err = conn.drive(response).await.expect_err("missing :status"); + assert!(err.is_reset()); + assert_eq!(err.reason(), Some(Reason::PROTOCOL_ERROR)); + } + + let request = Request::get("https://example.com/").body(()).unwrap(); + let (response, _) = client.send_request(request, true).unwrap(); + assert_eq!(conn.drive(response).await.unwrap().status(), 204); + }; + + tokio::time::timeout(std::time::Duration::from_secs(5), join(srv, client)) + .await + .expect("response_missing_status timed out"); +} + #[tokio::test] async fn recv_too_big_headers() { h2_support::trace_init!(); diff --git a/tests/h2-tests/tests/stream_states.rs b/tests/h2-tests/tests/stream_states.rs index 531f39d0a..e302f2328 100644 --- a/tests/h2-tests/tests/stream_states.rs +++ b/tests/h2-tests/tests/stream_states.rs @@ -1482,7 +1482,7 @@ async fn srv_window_update_on_lower_stream_id() { frames::push_promise(7, 2).request("GET", "https://http2.akamai.com/style.css"), ) .await; - srv.send_frame(frames::headers(7).eos()).await; + srv.send_frame(frames::headers(7).response(200).eos()).await; srv.recv_frame(frames::reset(2).cancel()).await; srv.send_frame(frames::window_update(5, 66666)).await; }; From 81f3f5baa741ae671596335817fd5b17097082f9 Mon Sep 17 00:00:00 2001 From: gngpp Date: Sat, 12 Sep 2026 23:32:30 +0800 Subject: [PATCH 2/2] style: fmt code --- tests/h2-tests/tests/client_request.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/h2-tests/tests/client_request.rs b/tests/h2-tests/tests/client_request.rs index 2ade37a66..7b8ae7849 100644 --- a/tests/h2-tests/tests/client_request.rs +++ b/tests/h2-tests/tests/client_request.rs @@ -860,13 +860,24 @@ async fn response_missing_status() { let client = async move { let (mut client, mut conn) = client::handshake(io).await.unwrap(); - for _ in 0..2 { - let request = Request::get("https://example.com/").body(()).unwrap(); - let (response, _) = client.send_request(request, true).unwrap(); - let err = conn.drive(response).await.expect_err("missing :status"); - assert!(err.is_reset()); - assert_eq!(err.reason(), Some(Reason::PROTOCOL_ERROR)); - } + + let request = Request::get("https://example.com/").body(()).unwrap(); + let (response, _) = client.send_request(request, true).unwrap(); + let err = conn + .drive(response) + .await + .expect_err("stream 1: empty response without :status"); + assert!(err.is_reset()); + assert_eq!(err.reason(), Some(Reason::PROTOCOL_ERROR)); + + let request = Request::get("https://example.com/").body(()).unwrap(); + let (response, _) = client.send_request(request, true).unwrap(); + let err = conn + .drive(response) + .await + .expect_err("stream 3: final response without :status after 103"); + assert!(err.is_reset()); + assert_eq!(err.reason(), Some(Reason::PROTOCOL_ERROR)); let request = Request::get("https://example.com/").body(()).unwrap(); let (response, _) = client.send_request(request, true).unwrap();