Adds a way to notify the `Http2Adapter` when the underlying codec decides not to send a frame.
This will help prevent a potential leak of `SelfDeletingMetadataSource` objects.
Protected by not protected; does not affect the GFE binary.
PiperOrigin-RevId: 615204887
diff --git a/quiche/http2/adapter/http2_adapter.h b/quiche/http2/adapter/http2_adapter.h
index 0ab0a72..817d4ea 100644
--- a/quiche/http2/adapter/http2_adapter.h
+++ b/quiche/http2/adapter/http2_adapter.h
@@ -143,6 +143,11 @@
// the stream was successfully resumed.
virtual bool ResumeStream(Http2StreamId stream_id) = 0;
+ // Called to communicate that a frame on a stream will not be sent.
+ // TODO(birenroy): remove when removing support for nghttp2.
+ virtual void FrameNotSent(Http2StreamId /*stream_id*/,
+ uint8_t /*frame_type*/) {}
+
protected:
// Subclasses should expose a public factory method for constructing and
// initializing (via Initialize()) adapter instances.
diff --git a/quiche/http2/adapter/nghttp2_adapter.cc b/quiche/http2/adapter/nghttp2_adapter.cc
index f724ea3..ded8307 100644
--- a/quiche/http2/adapter/nghttp2_adapter.cc
+++ b/quiche/http2/adapter/nghttp2_adapter.cc
@@ -19,11 +19,16 @@
using ConnectionError = Http2VisitorInterface::ConnectionError;
-// A metadata source that deletes itself upon completion.
-class SelfDeletingMetadataSource : public MetadataSource {
+} // anonymous namespace
+
+// A metadata source that notifies the owning NgHttp2Adapter upon completion or
+// failure.
+class NgHttp2Adapter::NotifyingMetadataSource : public MetadataSource {
public:
- explicit SelfDeletingMetadataSource(std::unique_ptr<MetadataSource> source)
- : source_(std::move(source)) {}
+ explicit NotifyingMetadataSource(NgHttp2Adapter* adapter,
+ Http2StreamId stream_id,
+ std::unique_ptr<MetadataSource> source)
+ : adapter_(adapter), stream_id_(stream_id), source_(std::move(source)) {}
size_t NumFrames(size_t max_frame_size) const override {
return source_->NumFrames(max_frame_size);
@@ -32,22 +37,22 @@
std::pair<int64_t, bool> Pack(uint8_t* dest, size_t dest_len) override {
const auto result = source_->Pack(dest, dest_len);
if (result.first < 0 || result.second) {
- delete this;
+ adapter_->RemovePendingMetadata(stream_id_);
}
return result;
}
void OnFailure() override {
source_->OnFailure();
- delete this;
+ adapter_->RemovePendingMetadata(stream_id_);
}
private:
+ NgHttp2Adapter* const adapter_;
+ const Http2StreamId stream_id_;
std::unique_ptr<MetadataSource> source_;
};
-} // anonymous namespace
-
/* static */
std::unique_ptr<NgHttp2Adapter> NgHttp2Adapter::CreateClientAdapter(
Http2VisitorInterface& visitor, const nghttp2_option* options) {
@@ -128,13 +133,15 @@
void NgHttp2Adapter::SubmitMetadata(Http2StreamId stream_id,
size_t max_frame_size,
std::unique_ptr<MetadataSource> source) {
- auto* wrapped_source = new SelfDeletingMetadataSource(std::move(source));
+ auto wrapped_source = std::make_unique<NotifyingMetadataSource>(
+ this, stream_id, std::move(source));
const size_t num_frames = wrapped_source->NumFrames(max_frame_size);
size_t num_successes = 0;
for (size_t i = 1; i <= num_frames; ++i) {
- const int result = nghttp2_submit_extension(
- session_->raw_ptr(), kMetadataFrameType,
- i == num_frames ? kMetadataEndFlag : 0, stream_id, wrapped_source);
+ const int result =
+ nghttp2_submit_extension(session_->raw_ptr(), kMetadataFrameType,
+ i == num_frames ? kMetadataEndFlag : 0,
+ stream_id, wrapped_source.get());
if (result != 0) {
QUICHE_LOG(DFATAL) << "Failed to submit extension frame " << i << " of "
<< num_frames;
@@ -142,8 +149,11 @@
}
++num_successes;
}
- if (num_successes == 0) {
- delete wrapped_source;
+ if (num_successes > 0) {
+ // Finds the MetadataSourceVec for `stream_id` or inserts a new one if not
+ // present.
+ auto [it, _] = stream_metadata_.insert({stream_id, MetadataSourceVec{}});
+ it->second.push_back(std::move(wrapped_source));
}
}
@@ -268,6 +278,12 @@
return 0 == nghttp2_session_resume_data(session_->raw_ptr(), stream_id);
}
+void NgHttp2Adapter::FrameNotSent(Http2StreamId stream_id, uint8_t frame_type) {
+ if (frame_type == kMetadataFrameType) {
+ RemovePendingMetadata(stream_id);
+ }
+}
+
void NgHttp2Adapter::RemoveStream(Http2StreamId stream_id) {
sources_.erase(stream_id);
}
@@ -305,5 +321,15 @@
options_ = nullptr;
}
+void NgHttp2Adapter::RemovePendingMetadata(Http2StreamId stream_id) {
+ auto it = stream_metadata_.find(stream_id);
+ if (it != stream_metadata_.end()) {
+ it->second.erase(it->second.begin());
+ if (it->second.empty()) {
+ stream_metadata_.erase(it);
+ }
+ }
+}
+
} // namespace adapter
} // namespace http2
diff --git a/quiche/http2/adapter/nghttp2_adapter.h b/quiche/http2/adapter/nghttp2_adapter.h
index 7ae9645..9337bb9 100644
--- a/quiche/http2/adapter/nghttp2_adapter.h
+++ b/quiche/http2/adapter/nghttp2_adapter.h
@@ -4,6 +4,7 @@
#include <cstdint>
#include "absl/container/flat_hash_map.h"
+#include "absl/container/inlined_vector.h"
#include "quiche/http2/adapter/http2_adapter.h"
#include "quiche/http2/adapter/http2_protocol.h"
#include "quiche/http2/adapter/nghttp2_session.h"
@@ -87,13 +88,25 @@
bool ResumeStream(Http2StreamId stream_id) override;
+ void FrameNotSent(Http2StreamId stream_id, uint8_t frame_type) override;
+
// Removes references to the `stream_id` from this adapter.
void RemoveStream(Http2StreamId stream_id);
// Accessor for testing.
size_t sources_size() const { return sources_.size(); }
+ size_t stream_metadata_size() const { return stream_metadata_.size(); }
+ size_t pending_metadata_count(Http2StreamId stream_id) const {
+ if (auto it = stream_metadata_.find(stream_id);
+ it != stream_metadata_.end()) {
+ return it->second.size();
+ }
+ return 0;
+ }
private:
+ class NotifyingMetadataSource;
+
NgHttp2Adapter(Http2VisitorInterface& visitor, Perspective perspective,
const nghttp2_option* options);
@@ -101,11 +114,18 @@
// such as preparing initial SETTINGS.
void Initialize();
+ void RemovePendingMetadata(Http2StreamId stream_id);
+
std::unique_ptr<NgHttp2Session> session_;
Http2VisitorInterface& visitor_;
const nghttp2_option* options_;
Perspective perspective_;
+ using MetadataSourceVec =
+ absl::InlinedVector<std::unique_ptr<MetadataSource>, 2>;
+ using MetadataMap = absl::flat_hash_map<Http2StreamId, MetadataSourceVec>;
+ MetadataMap stream_metadata_;
+
absl::flat_hash_map<int32_t, std::unique_ptr<DataFrameSource>> sources_;
};
diff --git a/quiche/http2/adapter/nghttp2_adapter_test.cc b/quiche/http2/adapter/nghttp2_adapter_test.cc
index 27770f5..2990190 100644
--- a/quiche/http2/adapter/nghttp2_adapter_test.cc
+++ b/quiche/http2/adapter/nghttp2_adapter_test.cc
@@ -2645,6 +2645,66 @@
EXPECT_FALSE(adapter->want_write());
}
+TEST(NgHttp2AdapterTest, ClientSubmitMetadataWithGoaway) {
+ DataSavingVisitor visitor;
+ auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
+
+ adapter->SubmitSettings({});
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, _, _, 0x0));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, _, _, 0x0, 0));
+ adapter->Send();
+
+ const std::vector<Header> headers =
+ ToHeaders({{":method", "GET"},
+ {":scheme", "http"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}});
+ const int32_t stream_id = adapter->SubmitRequest(headers, nullptr, nullptr);
+
+ auto source = std::make_unique<TestMetadataSource>(ToHeaderBlock(ToHeaders(
+ {{"query-cost", "is too darn high"}, {"secret-sauce", "hollandaise"}})));
+ adapter->SubmitMetadata(stream_id, 16384u, std::move(source));
+ EXPECT_TRUE(adapter->want_write());
+
+ const std::string initial_frames =
+ TestFrameSequence()
+ .ServerPreface()
+ .GoAway(3, Http2ErrorCode::HTTP2_NO_ERROR, "server shutting down")
+ .Serialize();
+ testing::InSequence s;
+
+ // Server preface
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ EXPECT_CALL(visitor, OnFrameHeader(0, _, GOAWAY, 0));
+ EXPECT_CALL(visitor, OnGoAway(3, Http2ErrorCode::HTTP2_NO_ERROR, _));
+
+ const int64_t initial_result = adapter->ProcessBytes(initial_frames);
+ EXPECT_EQ(initial_frames.size(), initial_result);
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+ // HEADERS frame is not sent.
+ EXPECT_CALL(visitor,
+ OnBeforeFrameSent(kMetadataFrameType, stream_id, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(kMetadataFrameType, stream_id, _, 0x4, 0));
+ EXPECT_CALL(visitor,
+ OnCloseStream(stream_id, Http2ErrorCode::REFUSED_STREAM));
+
+ int result = adapter->Send();
+ EXPECT_EQ(0, result);
+ absl::string_view serialized = visitor.data();
+ EXPECT_THAT(serialized,
+ testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix));
+ serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix));
+ EXPECT_THAT(serialized,
+ EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::SETTINGS,
+ static_cast<SpdyFrameType>(kMetadataFrameType)}));
+ EXPECT_FALSE(adapter->want_write());
+}
+
TEST(NgHttp2AdapterTest, ClientObeysMaxConcurrentStreams) {
DataSavingVisitor visitor;
auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
@@ -6614,6 +6674,84 @@
SpdyFrameType::RST_STREAM}));
}
+TEST(NgHttp2AdapterTest, ServerQueuesMetadataWithStreamReset) {
+ DataSavingVisitor visitor;
+ auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
+
+ const std::string initial_frames =
+ TestFrameSequence()
+ .ClientPreface()
+ .Headers(1,
+ {{":method", "GET"},
+ {":scheme", "http"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}},
+ /*fin=*/false)
+ .Serialize();
+
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0x4));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4);
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+
+ const int64_t initial_result = adapter->ProcessBytes(initial_frames);
+ EXPECT_EQ(static_cast<size_t>(initial_result), initial_frames.size());
+
+ auto body = std::make_unique<TestDataFrameSource>(visitor, true);
+ body->AppendPayload("Here is some data, which will be completely ignored!");
+
+ int submit_result = adapter->SubmitResponse(
+ 1, ToHeaders({{":status", "200"}}), std::move(body));
+ ASSERT_EQ(0, submit_result);
+
+ auto source = std::make_unique<TestMetadataSource>(ToHeaderBlock(ToHeaders(
+ {{"query-cost", "is too darn high"}, {"secret-sauce", "hollandaise"}})));
+ adapter->SubmitMetadata(1, 16384u, std::move(source));
+ adapter->SubmitWindowUpdate(1, 1024);
+
+ const std::string reset_frame =
+ TestFrameSequence().RstStream(1, Http2ErrorCode::CANCEL).Serialize();
+
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, RST_STREAM, 0x0));
+ EXPECT_CALL(visitor, OnRstStream(1, Http2ErrorCode::CANCEL));
+ EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::CANCEL));
+ adapter->ProcessBytes(reset_frame);
+
+ source = std::make_unique<TestMetadataSource>(
+ ToHeaderBlock(ToHeaders({{"really-important", "information!"}})));
+ adapter->SubmitMetadata(1, 16384u, std::move(source));
+
+ EXPECT_EQ(1, adapter->stream_metadata_size());
+ EXPECT_EQ(2, adapter->pending_metadata_count(1));
+
+ // Server initial SETTINGS and SETTINGS ack.
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0));
+
+ // nghttp2 apparently allows extension frames to be sent on reset streams.
+ // The response HEADERS, DATA and WINDOW_UPDATE are all discarded.
+ EXPECT_CALL(visitor, OnBeforeFrameSent(kMetadataFrameType, 1, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(kMetadataFrameType, 1, _, 0x4, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(kMetadataFrameType, 1, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(kMetadataFrameType, 1, _, 0x4, 0));
+
+ int send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(),
+ EqualsFrames({SpdyFrameType::SETTINGS,
+ static_cast<SpdyFrameType>(kMetadataFrameType),
+ static_cast<SpdyFrameType>(kMetadataFrameType)}));
+
+ EXPECT_EQ(0, adapter->stream_metadata_size());
+ EXPECT_EQ(0, adapter->pending_metadata_count(1));
+}
+
TEST(NgHttp2AdapterTest, ServerStartsShutdown) {
DataSavingVisitor visitor;
auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);