Adds test cases exercising response-complete-before-request for nghttp2 and oghttp2. Behavior appears identical in these cases. PiperOrigin-RevId: 681194104
diff --git a/quiche/http2/adapter/nghttp2_adapter_test.cc b/quiche/http2/adapter/nghttp2_adapter_test.cc index f7741de..d8b30fd 100644 --- a/quiche/http2/adapter/nghttp2_adapter_test.cc +++ b/quiche/http2/adapter/nghttp2_adapter_test.cc
@@ -594,6 +594,129 @@ SpdyFrameType::RST_STREAM})); } +class ResponseCompleteBeforeRequestTest + : public quiche::test::QuicheTestWithParam<std::tuple<bool, bool>> { + public: + bool HasTrailers() const { return std::get<0>(GetParam()); } + bool HasRstStream() const { return std::get<1>(GetParam()); } +}; + +INSTANTIATE_TEST_SUITE_P(TrailersAndRstStreamAllCombinations, + ResponseCompleteBeforeRequestTest, + testing::Combine(testing::Bool(), testing::Bool())); + +TEST_P(ResponseCompleteBeforeRequestTest, + ClientHandlesResponseBeforeRequestComplete) { + TestVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<Header> headers1 = + ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + adapter->SubmitSettings({}); + + auto body1 = std::make_unique<VisitorDataSource>(visitor, 1); + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, std::move(body1), false, nullptr); + ASSERT_GT(stream_id1, 0); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, + OnBeforeFrameSent(HEADERS, stream_id1, _, END_HEADERS_FLAG)); + EXPECT_CALL(visitor, + OnFrameSent(HEADERS, stream_id1, _, END_HEADERS_FLAG, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + visitor.Clear(); + + // * The server sends a complete response on stream 1 before the client has + // finished sending the request. + // * If HasTrailers(), the response ends with trailing HEADERS. + // * If HasRstStream(), the response is followed by a RST_STREAM NO_ERROR, + // as the HTTP/2 spec recommends. + TestFrameSequence response; + response.ServerPreface() + .Headers(1, {{":status", "200"}, {"content-length", "2"}}, + /*fin=*/false) + .Data(1, "hi", /*fin=*/!HasTrailers(), /*padding_length=*/10); + if (HasTrailers()) { + response.Headers(1, {{"my-weird-trailer", "has a value"}}, /*fin=*/true); + } + if (HasRstStream()) { + response.RstStream(1, Http2ErrorCode::HTTP2_NO_ERROR); + } + const std::string stream_frames = response.Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + // HEADERS for stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "content-length", "2")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + // DATA frame with padding for stream 1 + EXPECT_CALL(visitor, + OnFrameHeader(1, 2 + 10, DATA, HasTrailers() ? 0x8 : 0x9)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 2 + 10)); + EXPECT_CALL(visitor, OnDataForStream(1, "hi")); + EXPECT_CALL(visitor, OnDataPaddingLength(1, 10)); + if (HasTrailers()) { + // Trailers for stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "my-weird-trailer", "has a value")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + } + // END_STREAM for stream 1 + EXPECT_CALL(visitor, OnEndStream(1)); + if (HasRstStream()) { + EXPECT_CALL(visitor, OnFrameHeader(1, _, RST_STREAM, 0)); + EXPECT_CALL(visitor, OnRstStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + } + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, ACK_FLAG)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, ACK_FLAG, 0)); + + EXPECT_TRUE(adapter->want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({ + SpdyFrameType::SETTINGS, + })); + + // Stream 1 is done in the request direction. + if (!HasRstStream()) { + visitor.AppendPayloadForStream(1, "final fragment"); + } + visitor.SetEndData(1, true); + adapter->ResumeStream(1); + + if (!HasRstStream()) { + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, END_STREAM_FLAG, 0)); + // The codec reports Stream 1 as closed. + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + } + + result = adapter->Send(); + EXPECT_EQ(0, result); +} + TEST(NgHttp2AdapterTest, ClientHandles204WithContent) { TestVisitor visitor; auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor);
diff --git a/quiche/http2/adapter/oghttp2_adapter_test.cc b/quiche/http2/adapter/oghttp2_adapter_test.cc index 70e3192..3ba2bd4 100644 --- a/quiche/http2/adapter/oghttp2_adapter_test.cc +++ b/quiche/http2/adapter/oghttp2_adapter_test.cc
@@ -1215,6 +1215,129 @@ })); } +class ResponseCompleteBeforeRequestTest + : public quiche::test::QuicheTestWithParam<std::tuple<bool, bool>> { + public: + bool HasTrailers() const { return std::get<0>(GetParam()); } + bool HasRstStream() const { return std::get<1>(GetParam()); } +}; + +INSTANTIATE_TEST_SUITE_P(TrailersAndRstStreamAllCombinations, + ResponseCompleteBeforeRequestTest, + testing::Combine(testing::Bool(), testing::Bool())); + +TEST_P(ResponseCompleteBeforeRequestTest, + ClientHandlesResponseBeforeRequestComplete) { + TestVisitor visitor; + OgHttp2Adapter::Options options; + options.perspective = Perspective::kClient; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<Header> headers1 = + ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + auto body1 = std::make_unique<VisitorDataSource>(visitor, 1); + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, std::move(body1), false, nullptr); + ASSERT_GT(stream_id1, 0); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, + OnBeforeFrameSent(HEADERS, stream_id1, _, END_HEADERS_FLAG)); + EXPECT_CALL(visitor, + OnFrameSent(HEADERS, stream_id1, _, END_HEADERS_FLAG, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + visitor.Clear(); + + // * The server sends a complete response on stream 1 before the client has + // finished sending the request. + // * If HasTrailers(), the response ends with trailing HEADERS. + // * If HasRstStream(), the response is followed by a RST_STREAM NO_ERROR, + // as the HTTP/2 spec recommends. + TestFrameSequence response; + response.ServerPreface() + .Headers(1, {{":status", "200"}, {"content-length", "2"}}, + /*fin=*/false) + .Data(1, "hi", /*fin=*/!HasTrailers(), /*padding_length=*/10); + if (HasTrailers()) { + response.Headers(1, {{"my-weird-trailer", "has a value"}}, /*fin=*/true); + } + if (HasRstStream()) { + response.RstStream(1, Http2ErrorCode::HTTP2_NO_ERROR); + } + const std::string stream_frames = response.Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + // HEADERS for stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "content-length", "2")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + // DATA frame with padding for stream 1 + EXPECT_CALL(visitor, + OnFrameHeader(1, 2 + 10, DATA, HasTrailers() ? 0x8 : 0x9)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 2 + 10)); + EXPECT_CALL(visitor, OnDataPaddingLength(1, 10)); + EXPECT_CALL(visitor, OnDataForStream(1, "hi")); + if (HasTrailers()) { + // Trailers for stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "my-weird-trailer", "has a value")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + } + // END_STREAM for stream 1 + EXPECT_CALL(visitor, OnEndStream(1)); + if (HasRstStream()) { + EXPECT_CALL(visitor, OnFrameHeader(1, _, RST_STREAM, 0)); + EXPECT_CALL(visitor, OnRstStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + } + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, ACK_FLAG)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, ACK_FLAG, 0)); + + EXPECT_TRUE(adapter->want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({ + SpdyFrameType::SETTINGS, + })); + + // Stream 1 is done in the request direction. + if (!HasRstStream()) { + visitor.AppendPayloadForStream(1, "final fragment"); + } + visitor.SetEndData(1, true); + adapter->ResumeStream(1); + + if (!HasRstStream()) { + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, END_STREAM_FLAG, 0)); + // The codec reports Stream 1 as closed. + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + } + + result = adapter->Send(); + EXPECT_EQ(0, result); +} + TEST(OgHttp2AdapterTest, ClientHandles204WithContent) { TestVisitor visitor; OgHttp2Adapter::Options options;