Skip to content

Commit 2bb5d80

Browse files
committed
fix: filter stream initiator in recv_go_away (#885)
recv_go_away closes all streams with id > last_stream_id, regardless of which endpoint initiated them. When a client sends GOAWAY(0) to a server, client-initiated streams (e.g. stream 1) are incorrectly closed, killing in-progress response bodies before any DATA frames are sent. Add peer.is_local_init(stream.id) check so only locally-initiated streams above last_stream_id are closed, per RFC 9113 Section 6.8. Added test client_goaway_does_not_kill_remote_initiated_streams that verifies the server completes a streaming response body on stream 1 after receiving client GOAWAY(NO_ERROR, last_stream_id=0). The test fails without the fix and passes with it.
1 parent 5634ddd commit 2bb5d80

File tree

2 files changed

+65
-1
lines changed

2 files changed

+65
-1
lines changed

src/proto/streams/streams.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,8 +727,9 @@ impl Inner {
727727

728728
let err = Error::remote_go_away(frame.debug_data().clone(), frame.reason());
729729

730+
let peer = counts.peer();
730731
self.store.for_each(|stream| {
731-
if stream.id > last_stream_id {
732+
if stream.id > last_stream_id && peer.is_local_init(stream.id) {
732733
counts.transition(stream, |counts, stream| {
733734
actions.recv.handle_error(&err, &mut *stream);
734735
actions.send.handle_error(send_buffer, stream, counts);

tests/h2-tests/tests/server.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,69 @@ async fn goaway_even_if_client_sent_goaway() {
757757
join(client, srv).await;
758758
}
759759

760+
#[tokio::test]
761+
async fn client_goaway_does_not_kill_remote_initiated_streams() {
762+
h2_support::trace_init!();
763+
let (io, mut client) = mock::new();
764+
765+
let client = async move {
766+
let settings = client.assert_server_handshake().await;
767+
assert_default_settings!(settings);
768+
// Client sends a request on stream 1
769+
client
770+
.send_frame(
771+
frames::headers(1)
772+
.request("GET", "https://example.com/")
773+
.eos(),
774+
)
775+
.await;
776+
// Receive response headers (no END_STREAM)
777+
client.recv_frame(frames::headers(1).response(200)).await;
778+
// Client sends GOAWAY(0)
779+
client.send_frame(frames::go_away(0)).await;
780+
// Server should still be able to send the response body
781+
client
782+
.recv_frame(frames::data(1, "the response body").eos())
783+
.await;
784+
// Server sends its own GOAWAY and closes
785+
client.recv_frame(frames::go_away(1)).await;
786+
client.recv_eof().await;
787+
};
788+
789+
let srv = async move {
790+
let mut srv = server::handshake(io).await.expect("handshake");
791+
let (req, mut stream) = srv.next().await.unwrap().unwrap();
792+
assert_eq!(req.method(), &http::Method::GET);
793+
794+
// Send response headers without END_STREAM
795+
let rsp = http::Response::builder().status(200).body(()).unwrap();
796+
let mut tx = stream.send_response(rsp, false).unwrap();
797+
798+
// Drive the connection while sending the body.
799+
// The yields ensure the connection processes the client's GOAWAY
800+
// before we attempt to send data.
801+
let send_body = async {
802+
// First yield: connection flushes headers. Client receives them
803+
// and sends GOAWAY(0).
804+
tokio::task::yield_now().await;
805+
// Second yield: connection reads and processes GOAWAY(0).
806+
// Before the fix, stream 1 was killed here.
807+
tokio::task::yield_now().await;
808+
// Send response body. Before the fix, this failed because
809+
// stream 1 was incorrectly closed by recv_go_away.
810+
tx.send_data("the response body".into(), true).unwrap();
811+
};
812+
813+
let mut srv = Box::pin(async move {
814+
assert!(srv.next().await.is_none(), "unexpected request");
815+
});
816+
srv.drive(send_body).await;
817+
srv.await;
818+
};
819+
820+
join(client, srv).await;
821+
}
822+
760823
#[tokio::test]
761824
async fn sends_reset_cancel_when_res_body_is_dropped() {
762825
h2_support::trace_init!();

0 commit comments

Comments
 (0)