Skip to content

Commit 5634ddd

Browse files
authored
fix: avoid RST before initial HEADERS on idle streams (#875)
1 parent 2aeb81f commit 5634ddd

2 files changed

Lines changed: 82 additions & 5 deletions

File tree

src/proto/streams/send.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,22 @@ impl Send {
251251
return;
252252
}
253253

254-
// Clear all pending outbound frames.
255-
// Note that we don't call `self.recv_err` because we want to enqueue
256-
// the reset frame before transitioning the stream inside
257-
// `reclaim_all_capacity`.
258-
self.prioritize.clear_queue(buffer, stream);
254+
// If the stream hasn't been opened yet (its initial HEADERS are still
255+
// sitting in `pending_open`/`pending_send`), clearing the queue would
256+
// drop those HEADERS and let a RST_STREAM become the first frame on an
257+
// idle stream. HTTP/2 forbids that: §5.1 allows only HEADERS/PRIORITY
258+
// on idle streams and §6.4 says RST_STREAM on idle is a PROTOCOL_ERROR.
259+
// Keep the queued HEADERS so the stream opens, then send the reset
260+
// immediately after.
261+
if !stream.is_pending_open {
262+
// Otherwise, drop any buffered DATA/HEADERS and only send the
263+
// reset.
264+
//
265+
// Note that we don't call `self.recv_err` because we want to enqueue
266+
// the reset frame before transitioning the stream inside
267+
// `reclaim_all_capacity`.
268+
self.prioritize.clear_queue(buffer, stream);
269+
}
259270

260271
let frame = frame::Reset::new(stream.id, reason);
261272

tests/h2-tests/tests/client_request.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,72 @@ async fn server_drop_connection_after_go_away() {
19931993
join(srv, h2).await;
19941994
}
19951995

1996+
#[tokio::test]
1997+
async fn reset_before_headers_reaches_peer_without_headers() {
1998+
// Repro: body future errors immediately and hyper/h2 converts that into a
1999+
// RST_STREAM before the queued HEADERS are ever written, so the peer sees
2000+
// a reset for an idle stream and treats it as a PROTOCOL_ERROR.
2001+
h2_support::trace_init!();
2002+
2003+
let (io, srv) = mock::new();
2004+
2005+
// Server task: perform handshake then observe the first frame.
2006+
let srv = async move {
2007+
let mut srv = srv;
2008+
let settings = srv.assert_client_handshake().await;
2009+
assert_default_settings!(settings);
2010+
2011+
let frame = tokio::time::timeout(Duration::from_secs(1), srv.next())
2012+
.await
2013+
.expect("timed out waiting for first frame")
2014+
.expect("unexpected EOF")
2015+
.expect("frame error");
2016+
2017+
match frame {
2018+
frame::Frame::Headers(h) if h.stream_id() == StreamId::from(1) => {
2019+
assert!(h.is_end_stream() == false);
2020+
}
2021+
frame::Frame::Reset(rst) if rst.stream_id() == StreamId::from(1) => {
2022+
panic!(
2023+
"BUG: client sent RST_STREAM before any HEADERS on stream 1; reason={:?}",
2024+
rst.reason()
2025+
);
2026+
}
2027+
other => panic!("unexpected first frame: {:?}", other),
2028+
}
2029+
};
2030+
2031+
// Client task: queue HEADERS, immediately reset, then drive the connection.
2032+
let client = async move {
2033+
let (client, conn) = client::handshake(io).await.unwrap();
2034+
2035+
let req = Request::builder()
2036+
.method("POST")
2037+
.uri("https://example.com/")
2038+
.body(())
2039+
.unwrap();
2040+
let mut client = client.ready().await.expect("poll_ready");
2041+
let (_resp_fut, mut send_stream) = client.send_request(req, false).unwrap();
2042+
2043+
// Simulate body error (reqwest wraps into io::Error::Other) by resetting
2044+
// immediately after the stream is created.
2045+
send_stream.send_reset(Reason::INTERNAL_ERROR);
2046+
2047+
// Now start driving the connection so the queued frames get written.
2048+
let conn_task = tokio::spawn(async move {
2049+
let _ = conn.await;
2050+
});
2051+
2052+
// Give the connection a moment to flush frames.
2053+
tokio::time::sleep(Duration::from_millis(10)).await;
2054+
2055+
drop(send_stream);
2056+
let _ = conn_task.await;
2057+
};
2058+
2059+
join(srv, client).await;
2060+
}
2061+
19962062
const SETTINGS: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0];
19972063
const SETTINGS_ACK: &[u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0];
19982064

0 commit comments

Comments
 (0)