Fixes the conversion of a Span<const Header> to spdy::SpdyHeaderBlock in oghttp2 when the span contains multiple header fields with the same name.
PiperOrigin-RevId: 423107278
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc
index ff92a65..f2dbd67 100644
--- a/http2/adapter/nghttp2_adapter_test.cc
+++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -3254,6 +3254,67 @@
absl::StrJoin(visitor.GetMetadata(1), ""));
}
+TEST(NgHttp2AdapterTest, RepeatedHeaderNames) {
+ DataSavingVisitor visitor;
+ auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
+ EXPECT_FALSE(adapter->want_write());
+
+ const std::string frames = TestFrameSequence()
+ .ClientPreface()
+ .Headers(1,
+ {{":method", "GET"},
+ {":scheme", "https"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"},
+ {"accept", "text/plain"},
+ {"accept", "text/html"}},
+ /*fin=*/true)
+ .Serialize();
+
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ // Stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "text/plain"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "text/html"));
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+ EXPECT_CALL(visitor, OnEndStream(1));
+
+ int64_t result = adapter->ProcessBytes(frames);
+ EXPECT_EQ(frames.size(), static_cast<size_t>(result));
+
+ const std::vector<Header> headers1 = ToHeaders(
+ {{":status", "200"}, {"content-length", "10"}, {"content-length", "10"}});
+ auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true);
+ body1->AppendPayload("perfection");
+ body1->EndData();
+
+ int submit_result = adapter->SubmitResponse(1, headers1, std::move(body1));
+ ASSERT_EQ(0, submit_result);
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0));
+ EXPECT_CALL(visitor, OnFrameSent(DATA, 1, 10, 0x1, 0));
+ EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR));
+
+ int send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS,
+ spdy::SpdyFrameType::HEADERS,
+ spdy::SpdyFrameType::DATA}));
+}
+
TEST(NgHttp2AdapterTest, ServerRespondsToRequestWithTrailers) {
DataSavingVisitor visitor;
auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
@@ -3638,6 +3699,60 @@
EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
}
+TEST(NgHttp2AdapterInteractionTest,
+ ClientServerInteractionRepeatedHeaderNames) {
+ DataSavingVisitor client_visitor;
+ auto client_adapter = NgHttp2Adapter::CreateClientAdapter(client_visitor);
+
+ client_adapter->SubmitSettings({});
+
+ const std::vector<Header> headers1 =
+ ToHeaders({{":method", "GET"},
+ {":scheme", "http"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"},
+ {"accept", "text/plain"},
+ {"accept", "text/html"}});
+
+ const int32_t stream_id1 =
+ client_adapter->SubmitRequest(headers1, nullptr, nullptr);
+ ASSERT_GT(stream_id1, 0);
+
+ EXPECT_CALL(client_visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+ EXPECT_CALL(client_visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+ EXPECT_CALL(client_visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
+ EXPECT_CALL(client_visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
+ int send_result = client_adapter->Send();
+ EXPECT_EQ(0, send_result);
+
+ DataSavingVisitor server_visitor;
+ auto server_adapter = NgHttp2Adapter::CreateServerAdapter(server_visitor);
+
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(server_visitor, OnFrameHeader(0, _, SETTINGS, 0));
+ EXPECT_CALL(server_visitor, OnSettingsStart());
+ EXPECT_CALL(server_visitor, OnSetting(_)).Times(testing::AnyNumber());
+ EXPECT_CALL(server_visitor, OnSettingsEnd());
+ // Stream 1
+ EXPECT_CALL(server_visitor, OnFrameHeader(1, _, HEADERS, 5));
+ EXPECT_CALL(server_visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, ":scheme", "http"));
+ EXPECT_CALL(server_visitor,
+ OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(server_visitor,
+ OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, "accept", "text/plain"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, "accept", "text/html"));
+ EXPECT_CALL(server_visitor, OnEndHeadersForStream(1));
+ EXPECT_CALL(server_visitor, OnEndStream(1));
+
+ int64_t result = server_adapter->ProcessBytes(client_visitor.data());
+ EXPECT_EQ(client_visitor.data().size(), static_cast<size_t>(result));
+}
+
TEST(NgHttp2AdapterTest, ServerForbidsWindowUpdateOnIdleStream) {
DataSavingVisitor visitor;
auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc
index ee67139..d785891 100644
--- a/http2/adapter/oghttp2_adapter_test.cc
+++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -2637,6 +2637,71 @@
absl::StrJoin(visitor.GetMetadata(1), ""));
}
+TEST(OgHttp2AdapterServerTest, RepeatedHeaderNames) {
+ DataSavingVisitor visitor;
+ OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
+ auto adapter = OgHttp2Adapter::Create(visitor, options);
+ EXPECT_FALSE(adapter->want_write());
+
+ const std::string frames = TestFrameSequence()
+ .ClientPreface()
+ .Headers(1,
+ {{":method", "GET"},
+ {":scheme", "https"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"},
+ {"accept", "text/plain"},
+ {"accept", "text/html"}},
+ /*fin=*/true)
+ .Serialize();
+
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ // Stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "text/plain"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "text/html"));
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+ EXPECT_CALL(visitor, OnEndStream(1));
+
+ int64_t result = adapter->ProcessBytes(frames);
+ EXPECT_EQ(frames.size(), static_cast<size_t>(result));
+
+ const std::vector<Header> headers1 = ToHeaders(
+ {{":status", "200"}, {"content-length", "10"}, {"content-length", "10"}});
+ auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true);
+ body1->AppendPayload("perfection");
+ body1->EndData();
+
+ int submit_result = adapter->SubmitResponse(1, headers1, std::move(body1));
+ ASSERT_EQ(0, submit_result);
+
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+ EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+ EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4));
+ EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0));
+ EXPECT_CALL(visitor, OnFrameSent(DATA, 1, 10, 0x1, 0));
+ EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR));
+
+ int send_result = adapter->Send();
+ EXPECT_EQ(0, send_result);
+ EXPECT_THAT(visitor.data(),
+ EqualsFrames(
+ {spdy::SpdyFrameType::SETTINGS, spdy::SpdyFrameType::SETTINGS,
+ spdy::SpdyFrameType::HEADERS, spdy::SpdyFrameType::DATA}));
+}
+
TEST(OgHttp2AdapterServerTest, ServerRespondsToRequestWithTrailers) {
DataSavingVisitor visitor;
OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
@@ -3752,6 +3817,60 @@
client_adapter->Send();
}
+TEST(OgHttp2AdapterInteractionTest,
+ ClientServerInteractionRepeatedHeaderNames) {
+ DataSavingVisitor client_visitor;
+ OgHttp2Adapter::Options client_options{.perspective = Perspective::kClient};
+ auto client_adapter = OgHttp2Adapter::Create(client_visitor, client_options);
+
+ const std::vector<Header> headers1 =
+ ToHeaders({{":method", "GET"},
+ {":scheme", "http"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"},
+ {"accept", "text/plain"},
+ {"accept", "text/html"}});
+
+ const int32_t stream_id1 =
+ client_adapter->SubmitRequest(headers1, nullptr, nullptr);
+ ASSERT_GT(stream_id1, 0);
+
+ EXPECT_CALL(client_visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+ EXPECT_CALL(client_visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+ EXPECT_CALL(client_visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5));
+ EXPECT_CALL(client_visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0));
+ int send_result = client_adapter->Send();
+ EXPECT_EQ(0, send_result);
+
+ DataSavingVisitor server_visitor;
+ OgHttp2Adapter::Options server_options{.perspective = Perspective::kServer};
+ auto server_adapter = OgHttp2Adapter::Create(server_visitor, server_options);
+
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(server_visitor, OnFrameHeader(0, _, SETTINGS, 0));
+ EXPECT_CALL(server_visitor, OnSettingsStart());
+ EXPECT_CALL(server_visitor, OnSetting(_)).Times(testing::AnyNumber());
+ EXPECT_CALL(server_visitor, OnSettingsEnd());
+ // Stream 1
+ EXPECT_CALL(server_visitor, OnFrameHeader(1, _, HEADERS, 5));
+ EXPECT_CALL(server_visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, ":scheme", "http"));
+ EXPECT_CALL(server_visitor,
+ OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(server_visitor,
+ OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, "accept", "text/plain"));
+ EXPECT_CALL(server_visitor, OnHeaderForStream(1, "accept", "text/html"));
+ EXPECT_CALL(server_visitor, OnEndHeadersForStream(1));
+ EXPECT_CALL(server_visitor, OnEndStream(1));
+
+ int64_t result = server_adapter->ProcessBytes(client_visitor.data());
+ EXPECT_EQ(client_visitor.data().size(), static_cast<size_t>(result));
+}
+
TEST(OgHttp2AdapterServerTest, ServerForbidsNewStreamBelowWatermark) {
DataSavingVisitor visitor;
OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
diff --git a/http2/adapter/oghttp2_util.cc b/http2/adapter/oghttp2_util.cc
index 3ed13dc..ed0e6f6 100644
--- a/http2/adapter/oghttp2_util.cc
+++ b/http2/adapter/oghttp2_util.cc
@@ -8,7 +8,7 @@
for (const Header& header : headers) {
absl::string_view name = GetStringView(header.first).first;
absl::string_view value = GetStringView(header.second).first;
- block[name] = value;
+ block.AppendValueOrAddHeader(name, value);
}
return block;
}
diff --git a/http2/adapter/oghttp2_util_test.cc b/http2/adapter/oghttp2_util_test.cc
new file mode 100644
index 0000000..6f29b26
--- /dev/null
+++ b/http2/adapter/oghttp2_util_test.cc
@@ -0,0 +1,83 @@
+#include "http2/adapter/oghttp2_util.h"
+
+#include <utility>
+#include <vector>
+
+#include "http2/adapter/http2_protocol.h"
+#include "http2/adapter/test_frame_sequence.h"
+#include "common/platform/api/quiche_test.h"
+
+namespace http2 {
+namespace adapter {
+namespace test {
+namespace {
+
+using HeaderPair = std::pair<absl::string_view, absl::string_view>;
+
+TEST(ToHeaderBlock, EmptySpan) {
+ spdy::SpdyHeaderBlock block = ToHeaderBlock({});
+ EXPECT_TRUE(block.empty());
+}
+
+TEST(ToHeaderBlock, ExampleRequestHeaders) {
+ const std::vector<HeaderPair> pairs = {{":authority", "example.com"},
+ {":method", "GET"},
+ {":path", "/example.html"},
+ {":scheme", "http"},
+ {"accept", "text/plain, text/html"}};
+ const std::vector<Header> headers = ToHeaders(pairs);
+ spdy::SpdyHeaderBlock block = ToHeaderBlock(headers);
+ EXPECT_THAT(block, testing::ElementsAreArray(pairs));
+}
+
+TEST(ToHeaderBlock, ExampleResponseHeaders) {
+ const std::vector<HeaderPair> pairs = {
+ {":status", "403"},
+ {"content-length", "1023"},
+ {"x-extra-info", "humblest apologies"}};
+ const std::vector<Header> headers = ToHeaders(pairs);
+ spdy::SpdyHeaderBlock block = ToHeaderBlock(headers);
+ EXPECT_THAT(block, testing::ElementsAreArray(pairs));
+}
+
+TEST(ToHeaderBlock, RepeatedRequestHeaderNames) {
+ const std::vector<HeaderPair> pairs = {
+ {":authority", "example.com"}, {":method", "GET"},
+ {":path", "/example.html"}, {":scheme", "http"},
+ {"cookie", "chocolate_chips=yes"}, {"accept", "text/plain, text/html"},
+ {"cookie", "raisins=no"}};
+ const std::vector<HeaderPair> expected = {
+ {":authority", "example.com"},
+ {":method", "GET"},
+ {":path", "/example.html"},
+ {":scheme", "http"},
+ {"cookie", "chocolate_chips=yes; raisins=no"},
+ {"accept", "text/plain, text/html"}};
+ const std::vector<Header> headers = ToHeaders(pairs);
+ spdy::SpdyHeaderBlock block = ToHeaderBlock(headers);
+ EXPECT_THAT(block, testing::ElementsAreArray(expected));
+}
+
+TEST(ToHeaderBlock, RepeatedResponseHeaderNames) {
+ const std::vector<HeaderPair> pairs = {
+ {":status", "403"}, {"x-extra-info", "sorry"},
+ {"content-length", "1023"}, {"x-extra-info", "humblest apologies"},
+ {"content-length", "1024"}, {"set-cookie", "chocolate_chips=yes"},
+ {"set-cookie", "raisins=no"}};
+ const std::vector<HeaderPair> expected = {
+ {":status", "403"},
+ {"x-extra-info", absl::string_view("sorry\0humblest apologies", 24)},
+ {"content-length", absl::string_view("1023"
+ "\0"
+ "1024",
+ 9)},
+ {"set-cookie", absl::string_view("chocolate_chips=yes\0raisins=no", 30)}};
+ const std::vector<Header> headers = ToHeaders(pairs);
+ spdy::SpdyHeaderBlock block = ToHeaderBlock(headers);
+ EXPECT_THAT(block, testing::ElementsAreArray(expected));
+}
+
+} // namespace
+} // namespace test
+} // namespace adapter
+} // namespace http2