Skip to content

Commit 9af10d5

Browse files
diannahucopybara-github
authored andcommitted
Update oghttp2 to close streams above a received GOAWAY's last_stream_id.
This CL causes oghttp2 to respond to the receipt of a GOAWAY by: - Closing active streams in its stream map above the GOAWAY's last_stream_id. - Avoiding sending frames with stream ID values above the last_stream_id. These two changes handle cases where request HEADERS (or other frames for a stream) have already been enqueued at the time a GOAWAY is received, while also preventing subsequent enqueues for these active streams (by closing them). This CL does not handle the cases of new or pending streams, i.e., does not prevent subsequent requests or the start of requests that have been queued as pending. These cases will be handled in follow-up cl/439616811. This change increases oghttp2 parity with nghttp2. PiperOrigin-RevId: 440217657
1 parent 077951a commit 9af10d5

File tree

4 files changed

+196
-4
lines changed

4 files changed

+196
-4
lines changed

quiche/http2/adapter/nghttp2_adapter_test.cc

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,10 @@ TEST(NgHttp2AdapterTest, ClientReceivesGoAway) {
15851585
EqualsFrames({SpdyFrameType::HEADERS, SpdyFrameType::HEADERS}));
15861586
visitor.Clear();
15871587

1588+
// Submit a pending WINDOW_UPDATE for a stream that will be closed due to
1589+
// GOAWAY. The WINDOW_UPDATE should not be sent.
1590+
adapter->SubmitWindowUpdate(3, 42);
1591+
15881592
const std::string stream_frames =
15891593
TestFrameSequence()
15901594
.ServerPreface()
@@ -1611,11 +1615,90 @@ TEST(NgHttp2AdapterTest, ClientReceivesGoAway) {
16111615
const int64_t stream_result = adapter->ProcessBytes(stream_frames);
16121616
EXPECT_EQ(stream_frames.size(), stream_result);
16131617

1614-
// Intriguingly, despite want_write() being true, the sent data is empty; the
1615-
// receipt of the GOAWAY prevents the SETTINGS ack from being written.
1618+
EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
1619+
EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
1620+
1621+
// SETTINGS ack (but only after the enqueue of the seemingly unrelated
1622+
// WINDOW_UPDATE). The WINDOW_UPDATE is not written.
16161623
EXPECT_TRUE(adapter->want_write());
16171624
result = adapter->Send();
16181625
EXPECT_EQ(0, result);
1626+
EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS}));
1627+
}
1628+
1629+
TEST(NgHttp2AdapterTest, ClientReceivesMultipleGoAways) {
1630+
DataSavingVisitor visitor;
1631+
auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
1632+
1633+
testing::InSequence s;
1634+
1635+
const std::vector<Header> headers1 =
1636+
ToHeaders({{":method", "GET"},
1637+
{":scheme", "http"},
1638+
{":authority", "example.com"},
1639+
{":path", "/this/is/request/one"}});
1640+
1641+
const int32_t stream_id1 = adapter->SubmitRequest(headers1, nullptr, nullptr);
1642+
ASSERT_GT(stream_id1, 0);
1643+
1644+
EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
1645+
EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
1646+
1647+
int result = adapter->Send();
1648+
EXPECT_EQ(0, result);
1649+
absl::string_view data = visitor.data();
1650+
EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix));
1651+
data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix));
1652+
EXPECT_THAT(data, EqualsFrames({SpdyFrameType::HEADERS}));
1653+
visitor.Clear();
1654+
1655+
const std::string initial_frames =
1656+
TestFrameSequence()
1657+
.ServerPreface()
1658+
.GoAway(kMaxStreamId, Http2ErrorCode::INTERNAL_ERROR, "indigestion")
1659+
.Serialize();
1660+
1661+
EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
1662+
EXPECT_CALL(visitor, OnSettingsStart());
1663+
EXPECT_CALL(visitor, OnSettingsEnd());
1664+
EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
1665+
EXPECT_CALL(visitor, OnGoAway(kMaxStreamId, Http2ErrorCode::INTERNAL_ERROR,
1666+
"indigestion"));
1667+
1668+
const int64_t initial_result = adapter->ProcessBytes(initial_frames);
1669+
EXPECT_EQ(initial_frames.size(), static_cast<size_t>(initial_result));
1670+
1671+
// Submit a WINDOW_UPDATE for the open stream. Because the stream is below the
1672+
// GOAWAY's last_stream_id, it should be sent.
1673+
adapter->SubmitWindowUpdate(1, 42);
1674+
1675+
EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1));
1676+
EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0));
1677+
EXPECT_CALL(visitor, OnBeforeFrameSent(WINDOW_UPDATE, 1, 4, 0x0));
1678+
EXPECT_CALL(visitor, OnFrameSent(WINDOW_UPDATE, 1, 4, 0x0, 0));
1679+
1680+
result = adapter->Send();
1681+
EXPECT_EQ(0, result);
1682+
EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS,
1683+
SpdyFrameType::WINDOW_UPDATE}));
1684+
visitor.Clear();
1685+
1686+
const std::string final_frames =
1687+
TestFrameSequence()
1688+
.GoAway(0, Http2ErrorCode::INTERNAL_ERROR, "indigestion")
1689+
.Serialize();
1690+
1691+
EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
1692+
EXPECT_CALL(visitor,
1693+
OnGoAway(0, Http2ErrorCode::INTERNAL_ERROR, "indigestion"));
1694+
EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::REFUSED_STREAM));
1695+
1696+
const int64_t final_result = adapter->ProcessBytes(final_frames);
1697+
EXPECT_EQ(final_frames.size(), static_cast<size_t>(final_result));
1698+
1699+
EXPECT_FALSE(adapter->want_write());
1700+
result = adapter->Send();
1701+
EXPECT_EQ(0, result);
16191702
EXPECT_THAT(visitor.data(), testing::IsEmpty());
16201703
}
16211704

quiche/http2/adapter/oghttp2_adapter_test.cc

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,10 @@ TEST(OgHttp2AdapterTest, ClientReceivesGoAway) {
19551955
SpdyFrameType::HEADERS}));
19561956
visitor.Clear();
19571957

1958+
// Submit a pending WINDOW_UPDATE for a stream that will be closed due to
1959+
// GOAWAY. The WINDOW_UPDATE should not be sent.
1960+
adapter->SubmitWindowUpdate(3, 42);
1961+
19581962
const std::string stream_frames =
19591963
TestFrameSequence()
19601964
.ServerPreface()
@@ -1973,7 +1977,7 @@ TEST(OgHttp2AdapterTest, ClientReceivesGoAway) {
19731977
EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
19741978
// Currently, oghttp2 does not pass the opaque data to the visitor.
19751979
EXPECT_CALL(visitor, OnGoAway(1, Http2ErrorCode::INTERNAL_ERROR, ""));
1976-
// TODO(b/228093860): Close stream IDs above the GOAWAY's last_stream_id.
1980+
EXPECT_CALL(visitor, OnCloseStream(3, Http2ErrorCode::REFUSED_STREAM));
19771981
EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0));
19781982
EXPECT_CALL(visitor, OnWindowUpdate(0, 42));
19791983
EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0));
@@ -1990,6 +1994,87 @@ TEST(OgHttp2AdapterTest, ClientReceivesGoAway) {
19901994
EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS}));
19911995
}
19921996

1997+
TEST(OgHttp2AdapterTest, ClientReceivesMultipleGoAways) {
1998+
DataSavingVisitor visitor;
1999+
OgHttp2Adapter::Options options{.perspective = Perspective::kClient};
2000+
auto adapter = OgHttp2Adapter::Create(visitor, options);
2001+
2002+
testing::InSequence s;
2003+
2004+
const std::vector<Header> headers1 =
2005+
ToHeaders({{":method", "GET"},
2006+
{":scheme", "http"},
2007+
{":authority", "example.com"},
2008+
{":path", "/this/is/request/one"}});
2009+
2010+
const int32_t stream_id1 = adapter->SubmitRequest(headers1, nullptr, nullptr);
2011+
ASSERT_GT(stream_id1, 0);
2012+
2013+
EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
2014+
EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
2015+
EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
2016+
EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
2017+
2018+
int result = adapter->Send();
2019+
EXPECT_EQ(0, result);
2020+
absl::string_view data = visitor.data();
2021+
EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix));
2022+
data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix));
2023+
EXPECT_THAT(data,
2024+
EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::HEADERS}));
2025+
visitor.Clear();
2026+
2027+
const std::string initial_frames =
2028+
TestFrameSequence()
2029+
.ServerPreface()
2030+
.GoAway(kMaxStreamId, Http2ErrorCode::INTERNAL_ERROR, "indigestion")
2031+
.Serialize();
2032+
2033+
EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
2034+
EXPECT_CALL(visitor, OnSettingsStart());
2035+
EXPECT_CALL(visitor, OnSettingsEnd());
2036+
EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
2037+
// Currently, oghttp2 does not pass the opaque data to the visitor.
2038+
EXPECT_CALL(visitor,
2039+
OnGoAway(kMaxStreamId, Http2ErrorCode::INTERNAL_ERROR, ""));
2040+
2041+
const int64_t initial_result = adapter->ProcessBytes(initial_frames);
2042+
EXPECT_EQ(initial_frames.size(), static_cast<size_t>(initial_result));
2043+
2044+
// Submit a WINDOW_UPDATE for the open stream. Because the stream is below the
2045+
// GOAWAY's last_stream_id, it should be sent.
2046+
adapter->SubmitWindowUpdate(1, 42);
2047+
2048+
EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1));
2049+
EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0));
2050+
EXPECT_CALL(visitor, OnBeforeFrameSent(WINDOW_UPDATE, 1, 4, 0x0));
2051+
EXPECT_CALL(visitor, OnFrameSent(WINDOW_UPDATE, 1, 4, 0x0, 0));
2052+
2053+
result = adapter->Send();
2054+
EXPECT_EQ(0, result);
2055+
EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS,
2056+
SpdyFrameType::WINDOW_UPDATE}));
2057+
visitor.Clear();
2058+
2059+
const std::string final_frames =
2060+
TestFrameSequence()
2061+
.GoAway(0, Http2ErrorCode::INTERNAL_ERROR, "indigestion")
2062+
.Serialize();
2063+
2064+
EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
2065+
// Currently, oghttp2 does not pass the opaque data to the visitor.
2066+
EXPECT_CALL(visitor, OnGoAway(0, Http2ErrorCode::INTERNAL_ERROR, ""));
2067+
EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::REFUSED_STREAM));
2068+
2069+
const int64_t final_result = adapter->ProcessBytes(final_frames);
2070+
EXPECT_EQ(final_frames.size(), static_cast<size_t>(final_result));
2071+
2072+
EXPECT_FALSE(adapter->want_write());
2073+
result = adapter->Send();
2074+
EXPECT_EQ(0, result);
2075+
EXPECT_THAT(visitor.data(), testing::IsEmpty());
2076+
}
2077+
19932078
TEST(OgHttp2AdapterTest, ClientFailsOnGoAway) {
19942079
DataSavingVisitor visitor;
19952080
OgHttp2Adapter::Options options{.perspective = Perspective::kClient};

quiche/http2/adapter/oghttp2_session.cc

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <cstdint>
44
#include <utility>
5+
#include <vector>
56

67
#include "absl/memory/memory.h"
78
#include "absl/strings/escaping.h"
@@ -678,6 +679,13 @@ OgHttp2Session::SendResult OgHttp2Session::SendQueuedFrames() {
678679
DecrementQueuedFrameCount(c.stream_id(), c.frame_type());
679680
frames_.pop_front();
680681
continue;
682+
} else if (received_goaway_ &&
683+
c.stream_id() >
684+
static_cast<uint32_t>(received_goaway_stream_id_)) {
685+
// This frame will be ignored by the peer, so don't send it. The stream
686+
// should already have been closed.
687+
frames_.pop_front();
688+
continue;
681689
}
682690
// Frames can't accurately report their own length; the actual serialized
683691
// length must be used instead.
@@ -1288,12 +1296,28 @@ void OgHttp2Session::OnPing(spdy::SpdyPingId unique_id, bool is_ack) {
12881296
void OgHttp2Session::OnGoAway(spdy::SpdyStreamId last_accepted_stream_id,
12891297
spdy::SpdyErrorCode error_code) {
12901298
received_goaway_ = true;
1299+
// TODO(diannahu): Validate that `last_accepted_stream_id` is non-increasing.
1300+
received_goaway_stream_id_ = last_accepted_stream_id;
12911301
const bool result = visitor_.OnGoAway(last_accepted_stream_id,
12921302
TranslateErrorCode(error_code), "");
12931303
if (!result) {
12941304
fatal_visitor_callback_failure_ = true;
12951305
decoder_.StopProcessing();
12961306
}
1307+
1308+
// Close the streams above `last_accepted_stream_id`.
1309+
if (last_accepted_stream_id == spdy::kMaxStreamId) {
1310+
return;
1311+
}
1312+
std::vector<Http2StreamId> streams_to_close;
1313+
for (const auto& [stream_id, stream_state] : stream_map_) {
1314+
if (static_cast<spdy::SpdyStreamId>(stream_id) > last_accepted_stream_id) {
1315+
streams_to_close.push_back(stream_id);
1316+
}
1317+
}
1318+
for (Http2StreamId stream_id : streams_to_close) {
1319+
CloseStream(stream_id, Http2ErrorCode::REFUSED_STREAM);
1320+
}
12971321
}
12981322

12991323
bool OgHttp2Session::OnGoAwayFrameData(const char* /*goaway_data*/, size_t
@@ -1710,7 +1734,6 @@ void OgHttp2Session::CloseStream(Http2StreamId stream_id,
17101734
metadata_ready_.erase(stream_id);
17111735
streams_reset_.erase(stream_id);
17121736
queued_frames_.erase(stream_id);
1713-
stream_map_.erase(stream_id);
17141737
if (write_scheduler_.StreamRegistered(stream_id)) {
17151738
write_scheduler_.UnregisterStream(stream_id);
17161739
}

quiche/http2/adapter/oghttp2_session.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ class QUICHE_EXPORT_PRIVATE OgHttp2Session
498498
Http2StreamId highest_received_stream_id_ = 0;
499499
Http2StreamId highest_processed_stream_id_ = 0;
500500
Http2StreamId metadata_stream_id_ = 0;
501+
Http2StreamId received_goaway_stream_id_ = 0;
501502
size_t metadata_length_ = 0;
502503
int32_t connection_send_window_ = kInitialFlowControlWindowSize;
503504
// The initial flow control receive window size for any newly created streams.

0 commit comments

Comments
 (0)