diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 8a4e292af508b..70af4851b8e76 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -486,10 +486,10 @@ message Http2ProtocolOptions { // Allows proxying Websocket and other upgrades over H2 connect. bool allow_connect = 5; - // [#not-implemented-hide:] Hiding until envoy has full metadata support. + // [#not-implemented-hide:] Hiding until Envoy has full metadata support. // Still under implementation. DO NOT USE. // - // Allows metadata. See [metadata + // Allows sending and receiving HTTP/2 METADATA frames. See [metadata // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; @@ -618,7 +618,7 @@ message GrpcProtocolOptions { } // A message which allows using HTTP/3. -// [#next-free-field: 6] +// [#next-free-field: 7] message Http3ProtocolOptions { QuicProtocolOptions quic_protocol_options = 1; @@ -637,6 +637,14 @@ message Http3ProtocolOptions { // `_ // Note that HTTP/3 CONNECT is not yet an RFC. bool allow_extended_connect = 5 [(xds.annotations.v3.field_status).work_in_progress = true]; + + // [#not-implemented-hide:] Hiding until Envoy has full metadata support. + // Still under implementation. DO NOT USE. + // + // Allows sending and receiving HTTP/3 METADATA frames. See [metadata + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more + // information. + bool allow_metadata = 6; } // A message to control transformations to the :scheme header diff --git a/changelogs/current.yaml b/changelogs/current.yaml index e13ab82f5fb58..6d2dff7d9edbc 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -239,6 +239,9 @@ new_features: change: | Added :ref:`host_rewrite ` config to be used during signature. +- area: http3 + change: | + Added experimental support for sending and receiving HTTP/3 METADATA frames. - area: ext_proc change: | added diff --git a/source/common/quic/envoy_quic_client_stream.cc b/source/common/quic/envoy_quic_client_stream.cc index 18e02bb0bdc36..1e659ac61e4c0 100644 --- a/source/common/quic/envoy_quic_client_stream.cc +++ b/source/common/quic/envoy_quic_client_stream.cc @@ -23,7 +23,7 @@ EnvoyQuicClientStream::EnvoyQuicClientStream( const envoy::config::core::v3::Http3ProtocolOptions& http3_options) : quic::QuicSpdyClientStream(id, client_session, type), EnvoyQuicStream( - *this, + *this, *client_session, // Flow control receive window should be larger than 8k so that the send buffer can fully // utilize congestion control window before it reaches the high watermark. static_cast(GetReceiveWindow().value()), *filterManagerConnection(), @@ -31,6 +31,7 @@ EnvoyQuicClientStream::EnvoyQuicClientStream( stats, http3_options) { ASSERT(static_cast(GetReceiveWindow().value()) > 8 * 1024, "Send buffer limit should be larger than 8KB."); + RegisterMetadataVisitor(this); } Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers, @@ -411,6 +412,17 @@ QuicFilterManagerConnectionImpl* EnvoyQuicClientStream::filterManagerConnection( return dynamic_cast(session()); } +void EnvoyQuicClientStream::OnMetadataComplete(size_t /*frame_len*/, + const quic::QuicHeaderList& header_list) { + if (mustRejectMetadata(header_list.uncompressed_header_bytes())) { + onStreamError(true, quic::QUIC_HEADERS_TOO_LARGE); + return; + } + if (!header_list.empty()) { + response_decoder_->decodeMetadata(metadataMapFromHeaderList(header_list)); + } +} + void EnvoyQuicClientStream::onStreamError(absl::optional should_close_connection, quic::QuicRstStreamErrorCode rst_code) { if (details_.empty()) { diff --git a/source/common/quic/envoy_quic_client_stream.h b/source/common/quic/envoy_quic_client_stream.h index 98607867ea5be..2cb1c5105909e 100644 --- a/source/common/quic/envoy_quic_client_stream.h +++ b/source/common/quic/envoy_quic_client_stream.h @@ -9,6 +9,8 @@ #endif #include "quiche/common/simple_buffer_allocator.h" #include "quiche/quic/core/http/quic_spdy_client_stream.h" +#include "quiche/quic/core/qpack/qpack_encoder.h" +#include "quiche/quic/core/qpack/qpack_instruction_encoder.h" namespace Envoy { namespace Quic { @@ -16,7 +18,8 @@ namespace Quic { // This class is a quic stream and also a request encoder. class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, public EnvoyQuicStream, - public Http::RequestEncoder { + public Http::RequestEncoder, + public quic::QuicSpdyStream::MetadataVisitor { public: EnvoyQuicClientStream(quic::QuicStreamId id, quic::QuicSpdyClientSession* client_session, quic::StreamType type, Http::Http3::CodecStats& stats, @@ -52,6 +55,9 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, void clearWatermarkBuffer(); + // quic::QuicSpdyStream::MetadataVisitor + void OnMetadataComplete(size_t frame_len, const quic::QuicHeaderList& header_list) override; + protected: // EnvoyQuicStream void switchStreamBlockState() override; diff --git a/source/common/quic/envoy_quic_server_stream.cc b/source/common/quic/envoy_quic_server_stream.cc index 1932010257f7c..94a82cce10d71 100644 --- a/source/common/quic/envoy_quic_server_stream.cc +++ b/source/common/quic/envoy_quic_server_stream.cc @@ -30,7 +30,7 @@ EnvoyQuicServerStream::EnvoyQuicServerStream( headers_with_underscores_action) : quic::QuicSpdyServerStreamBase(id, session, type), EnvoyQuicStream( - *this, + *this, *session, // Flow control receive window should be larger than 8k to fully utilize congestion // control window before it reaches the high watermark. static_cast(GetReceiveWindow().value()), *filterManagerConnection(), @@ -42,6 +42,7 @@ EnvoyQuicServerStream::EnvoyQuicServerStream( stats_gatherer_ = new QuicStatsGatherer(&filterManagerConnection()->dispatcher().timeSource()); set_ack_listener(stats_gatherer_); + RegisterMetadataVisitor(this); } void EnvoyQuicServerStream::encode1xxHeaders(const Http::ResponseHeaderMap& headers) { @@ -449,6 +450,17 @@ EnvoyQuicServerStream::validateHeader(absl::string_view header_name, return result; } +void EnvoyQuicServerStream::OnMetadataComplete(size_t /*frame_len*/, + const quic::QuicHeaderList& header_list) { + if (mustRejectMetadata(header_list.uncompressed_header_bytes())) { + onStreamError(true, quic::QUIC_HEADERS_TOO_LARGE); + return; + } + if (!header_list.empty()) { + request_decoder_->decodeMetadata(metadataMapFromHeaderList(header_list)); + } +} + void EnvoyQuicServerStream::onStreamError(absl::optional should_close_connection, quic::QuicRstStreamErrorCode rst) { if (details_.empty()) { diff --git a/source/common/quic/envoy_quic_server_stream.h b/source/common/quic/envoy_quic_server_stream.h index bb9c32003b070..9153217efcb18 100644 --- a/source/common/quic/envoy_quic_server_stream.h +++ b/source/common/quic/envoy_quic_server_stream.h @@ -8,6 +8,8 @@ #include "quiche/common/platform/api/quiche_reference_counted.h" #include "quiche/quic/core/http/quic_spdy_server_stream_base.h" +#include "quiche/quic/core/qpack/qpack_encoder.h" +#include "quiche/quic/core/qpack/qpack_instruction_encoder.h" namespace Envoy { namespace Quic { @@ -15,7 +17,8 @@ namespace Quic { // This class is a quic stream and also a response encoder. class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, public EnvoyQuicStream, - public Http::ResponseEncoder { + public Http::ResponseEncoder, + public quic::QuicSpdyStream::MetadataVisitor { public: EnvoyQuicServerStream(quic::QuicStreamId id, quic::QuicSpdySession* session, quic::StreamType type, Http::Http3::CodecStats& stats, @@ -78,6 +81,9 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, Http::HeaderUtility::HeaderValidationResult validateHeader(absl::string_view header_name, absl::string_view header_value) override; + // quic::QuicSpdyStream::MetadataVisitor + void OnMetadataComplete(size_t frame_len, const quic::QuicHeaderList& header_list) override; + protected: // EnvoyQuicStream void switchStreamBlockState() override; diff --git a/source/common/quic/envoy_quic_stream.cc b/source/common/quic/envoy_quic_stream.cc index 8ca1e7950466b..280c7ab915135 100644 --- a/source/common/quic/envoy_quic_stream.cc +++ b/source/common/quic/envoy_quic_stream.cc @@ -102,10 +102,87 @@ void EnvoyQuicStream::encodeTrailersImpl(spdy::Http2HeaderBlock&& trailers) { onLocalEndStream(); } -void EnvoyQuicStream::encodeMetadata(const Http::MetadataMapVector& /*metadata_map_vector*/) { - // Metadata Frame is not supported in QUICHE. - ENVOY_STREAM_LOG(debug, "METADATA is not supported in Http3.", *this); - stats_.metadata_not_supported_error_.inc(); +std::unique_ptr +EnvoyQuicStream::metadataMapFromHeaderList(const quic::QuicHeaderList& header_list) { + auto metadata_map = std::make_unique(); + for (const auto& [key, value] : header_list) { + (*metadata_map)[key] = value; + } + return metadata_map; +} + +namespace { + +// Returns a new `unique_ptr` containing the characters copied from `str`. +std::unique_ptr dataFromString(const std::string& str) { + auto data = std::make_unique(str.length()); + memcpy(&data[0], str.data(), str.length()); // NOLINT(safe-memcpy) + return data; +} + +void serializeMetadata(const Http::MetadataMapPtr& metadata, quic::QuicStreamId id, + absl::InlinedVector& slices) { + quic::NoopDecoderStreamErrorDelegate decoder_stream_error_delegate; + quic::QpackEncoder qpack_encoder(&decoder_stream_error_delegate, + quic::HuffmanEncoding::kDisabled); + + spdy::Http2HeaderBlock header_block; + for (const auto& [key, value] : *metadata) { + header_block.AppendValueOrAddHeader(key, value); + } + + // The METADATA frame consist of a frame header, which includes payload + // length, and a payload, which is the QPACK-encoded metadata block. In order + // to generate the frame header, the payload needs to be generated first. + std::string metadata_frame_payload = + qpack_encoder.EncodeHeaderList(id, header_block, + /* encoder_stream_sent_byte_count = */ nullptr); + std::string metadata_frame_header = + quic::HttpEncoder::SerializeMetadataFrameHeader(metadata_frame_payload.size()); + + slices.emplace_back(dataFromString(metadata_frame_header), metadata_frame_header.length()); + slices.emplace_back(dataFromString(metadata_frame_payload), metadata_frame_payload.length()); +} + +} // namespace + +void EnvoyQuicStream::encodeMetadata(const Http::MetadataMapVector& metadata_map_vector) { + if (!http3_options_.allow_metadata()) { + ENVOY_STREAM_LOG(debug, "METADATA not supported by config.", *this); + stats_.metadata_not_supported_error_.inc(); + return; + } + if (quic_stream_.write_side_closed()) { + return; + } + ASSERT(!local_end_stream_); + + for (const Http::MetadataMapPtr& metadata : metadata_map_vector) { + absl::InlinedVector quic_slices; + quic_slices.reserve(2); + serializeMetadata(metadata, quic_stream_.id(), quic_slices); + absl::Span metadata_frame(quic_slices); + + SendBufferMonitor::ScopedWatermarkBufferUpdater updater(&quic_stream_, this); + quic::QuicConsumedData result{0, false}; + { + IncrementalBytesSentTracker tracker(quic_stream_, *mutableBytesMeter(), false); + result = quic_stream_.WriteMemSlices(metadata_frame, /*end_stream=*/false); + } + // QUIC stream must take all. + if (result.bytes_consumed == 0) { + IS_ENVOY_BUG(fmt::format("Send buffer didn't take all the data. Stream is write {} with {} " + "bytes in send buffer. Current write was rejected.", + quic_stream_.write_side_closed() ? "closed" : "open", + quic_stream_.BufferedDataBytes())); + quic_stream_.Reset(quic::QUIC_ERROR_PROCESSING_STREAM); + return; + } + if (!quic_session_.connection()->connected()) { + // Return early if sending METADATA caused the connection to close. + return; + } + } } } // namespace Quic diff --git a/source/common/quic/envoy_quic_stream.h b/source/common/quic/envoy_quic_stream.h index 360788d2fdcdb..bbe4759339ae2 100644 --- a/source/common/quic/envoy_quic_stream.h +++ b/source/common/quic/envoy_quic_stream.h @@ -33,13 +33,13 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, public: // |buffer_limit| is the high watermark of the stream send buffer, and the low // watermark will be half of it. - EnvoyQuicStream(quic::QuicSpdyStream& quic_stream, uint32_t buffer_limit, - QuicFilterManagerConnectionImpl& filter_manager_connection, + EnvoyQuicStream(quic::QuicSpdyStream& quic_stream, quic::QuicSession& quic_session, + uint32_t buffer_limit, QuicFilterManagerConnectionImpl& filter_manager_connection, std::function below_low_watermark, std::function above_high_watermark, Http::Http3::CodecStats& stats, const envoy::config::core::v3::Http3ProtocolOptions& http3_options) : Http::MultiplexedStreamImplBase(filter_manager_connection.dispatcher()), stats_(stats), - http3_options_(http3_options), quic_stream_(quic_stream), + http3_options_(http3_options), quic_stream_(quic_stream), quic_session_(quic_session), send_buffer_simulation_(buffer_limit / 2, buffer_limit, std::move(below_low_watermark), std::move(above_high_watermark), ENVOY_LOGGER()), filter_manager_connection_(filter_manager_connection), @@ -180,6 +180,17 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, void encodeTrailersImpl(spdy::Http2HeaderBlock&& trailers); + // Converts `header_list` into a new `Http::MetadataMap`. + std::unique_ptr + metadataMapFromHeaderList(const quic::QuicHeaderList& header_list); + + // Returns true if the cumulative limit on METADATA headers has been reached + // after adding `bytes`. + bool mustRejectMetadata(size_t bytes) { + received_metadata_bytes_ += bytes; + return received_metadata_bytes_ > 1 << 20; + } + #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS // Setting |http_datagram_handler_| enables HTTP Datagram support. std::unique_ptr http_datagram_handler_; @@ -210,8 +221,9 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, bool saw_regular_headers_{false}; private: - // QUIC stream that this EnvoyQuicStream wraps. + // QUIC stream and session that this EnvoyQuicStream wraps. quic::QuicSpdyStream& quic_stream_; + quic::QuicSession& quic_session_; // Keeps track of bytes buffered in the stream send buffer in QUICHE and reacts // upon crossing high and low watermarks. @@ -232,6 +244,7 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, absl::optional content_length_; size_t received_content_bytes_{0}; http2::adapter::HeaderValidator header_validator_; + size_t received_metadata_bytes_{0}; }; // Object used for updating a BytesMeter to track bytes sent on a QuicStream since this object was diff --git a/source/common/quic/platform/quiche_flags_constants.h b/source/common/quic/platform/quiche_flags_constants.h index cff579bad68ce..a38bd99a0fc61 100644 --- a/source/common/quic/platform/quiche_flags_constants.h +++ b/source/common/quic/platform/quiche_flags_constants.h @@ -17,6 +17,8 @@ /* Envoy only supports RFC-v1 in the long term, so disable IETF draft 29 implementation by \ * default. */ \ KEY_VALUE_PAIR(quic_disable_version_draft_29, true) \ + /* Enable support for HTTP/3 metadata decoding in QUICHE. */ \ + KEY_VALUE_PAIR(quic_enable_http3_metadata_decoding, true) \ /* This flag enables BBR, otherwise QUIC will use Cubic which is less performant */ \ KEY_VALUE_PAIR(quic_default_to_bbr, true) diff --git a/source/common/quic/platform/quiche_flags_impl.cc b/source/common/quic/platform/quiche_flags_impl.cc index f676005f91b74..0f8d64fcfce95 100644 --- a/source/common/quic/platform/quiche_flags_impl.cc +++ b/source/common/quic/platform/quiche_flags_impl.cc @@ -117,7 +117,7 @@ FlagRegistry::FlagRegistry() : reloadable_flags_(makeReloadableFlagMap()) {} FlagRegistry& FlagRegistry::getInstance() { static auto* instance = new FlagRegistry(); ASSERT(sizeof(quiche_reloadable_flag_overrides) / sizeof(std::pair) == - 2); + 3); ASSERT(sizeof(quiche_protocol_flag_overrides) / sizeof(std::pair>) == 3); diff --git a/test/integration/fake_upstream.h b/test/integration/fake_upstream.h index 24a89f96d7c0f..78bc9b1566aa9 100644 --- a/test/integration/fake_upstream.h +++ b/test/integration/fake_upstream.h @@ -644,6 +644,7 @@ struct FakeUpstreamConfig { http2_options_.set_allow_connect(true); http2_options_.set_allow_metadata(true); http3_options_.set_allow_extended_connect(true); + http3_options_.set_allow_metadata(true); } Event::TestTimeSystem& time_system_; diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index c25103ffabb08..2064c9629c456 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -276,13 +276,14 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection( .value(); http2_options.value().set_allow_connect(true); http2_options.value().set_allow_metadata(true); + } #ifdef ENVOY_ENABLE_QUIC - } else { - cluster->http3_options_ = ConfigHelper::http2ToHttp3ProtocolOptions( - http2_options.value(), quic::kStreamReceiveWindowLimit); - cluster->http3_options_.set_allow_extended_connect(true); + cluster->http3_options_ = ConfigHelper::http2ToHttp3ProtocolOptions( + http2_options.value(), quic::kStreamReceiveWindowLimit); + cluster->http3_options_.set_allow_extended_connect(true); + cluster->http3_options_.set_allow_metadata(true); #endif - } + cluster->http2_options_ = http2_options.value(); cluster->http1_settings_.enable_trailers_ = true; diff --git a/test/integration/multiplexed_integration_test.cc b/test/integration/multiplexed_integration_test.cc index d83e2e395ef2c..4f534ecaf40d6 100644 --- a/test/integration/multiplexed_integration_test.cc +++ b/test/integration/multiplexed_integration_test.cc @@ -276,7 +276,7 @@ static std::string response_metadata_filter = R"EOF( name: response-metadata-filter )EOF"; -class Http2MetadataIntegrationTest : public HttpProtocolIntegrationTest { +class MetadataIntegrationTest : public HttpProtocolIntegrationTest { public: void SetUp() override { HttpProtocolIntegrationTest::SetUp(); @@ -284,15 +284,25 @@ class Http2MetadataIntegrationTest : public HttpProtocolIntegrationTest { [&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { RELEASE_ASSERT(bootstrap.mutable_static_resources()->clusters_size() >= 1, ""); ConfigHelper::HttpProtocolOptions protocol_options; - protocol_options.mutable_explicit_http_config() - ->mutable_http2_protocol_options() - ->set_allow_metadata(true); + if (GetParam().upstream_protocol == Http::CodecType::HTTP3) { + protocol_options.mutable_explicit_http_config() + ->mutable_http3_protocol_options() + ->set_allow_metadata(true); + protocol_options.mutable_upstream_http_protocol_options()->set_auto_sni(true); + } else { + protocol_options.mutable_explicit_http_config() + ->mutable_http2_protocol_options() + ->set_allow_metadata(true); + } ConfigHelper::setProtocolOptions( *bootstrap.mutable_static_resources()->mutable_clusters(0), protocol_options); }); config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& - hcm) -> void { hcm.mutable_http2_protocol_options()->set_allow_metadata(true); }); + hcm) -> void { + hcm.mutable_http2_protocol_options()->set_allow_metadata(true); + hcm.mutable_http3_protocol_options()->set_allow_metadata(true); + }); } void testRequestMetadataWithStopAllFilter(); @@ -301,6 +311,8 @@ class Http2MetadataIntegrationTest : public HttpProtocolIntegrationTest { void runHeaderOnlyTest(bool send_request_body, size_t body_size); + Http::CodecClient::Type upstreamProtocol() { return GetParam().upstream_protocol; } + protected: // Utility function to prepend filters. Note that the filters // are added in reverse order. @@ -312,7 +324,7 @@ class Http2MetadataIntegrationTest : public HttpProtocolIntegrationTest { }; // Verifies metadata can be sent at different locations of the responses. -TEST_P(Http2MetadataIntegrationTest, ProxyMetadataInResponse) { +TEST_P(MetadataIntegrationTest, ProxyMetadataInResponse) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -445,7 +457,7 @@ TEST_P(Http2MetadataIntegrationTest, ProxyMetadataInResponse) { test_server_->waitForCounterEq(counter, 1); } -TEST_P(Http2MetadataIntegrationTest, ProxyMultipleMetadata) { +TEST_P(MetadataIntegrationTest, ProxyMultipleMetadata) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -485,7 +497,7 @@ TEST_P(Http2MetadataIntegrationTest, ProxyMultipleMetadata) { // Disabled temporarily see #19040 #if 0 -TEST_P(Http2MetadataIntegrationTest, ProxyInvalidMetadata) { +TEST_P(MetadataIntegrationTest, ProxyInvalidMetadata) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -516,14 +528,16 @@ TEST_P(Http2MetadataIntegrationTest, ProxyInvalidMetadata) { #endif void verifyExpectedMetadata(Http::MetadataMap metadata_map, std::set keys) { + EXPECT_EQ(metadata_map.size(), keys.size()); for (const auto& key : keys) { // keys are the same as their corresponding values. + auto it = metadata_map.find(key); + ASSERT_FALSE(it == metadata_map.end()) << "key: " << key; EXPECT_EQ(metadata_map.find(key)->second, key); } - EXPECT_EQ(metadata_map.size(), keys.size()); } -TEST_P(Http2MetadataIntegrationTest, TestResponseMetadata) { +TEST_P(MetadataIntegrationTest, TestResponseMetadata) { prependFilters({response_metadata_filter}); config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -540,6 +554,11 @@ TEST_P(Http2MetadataIntegrationTest, TestResponseMetadata) { ASSERT_TRUE(response->waitForEndStream()); ASSERT_TRUE(response->complete()); std::set expected_metadata_keys = {"headers", "duplicate"}; + if (upstreamProtocol() == Http::CodecType::HTTP3) { + // HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter + // adding the "data" metadata header. + expected_metadata_keys.insert("data"); + } verifyExpectedMetadata(response->metadataMap(), expected_metadata_keys); // Upstream responds with headers and data. @@ -571,13 +590,9 @@ TEST_P(Http2MetadataIntegrationTest, TestResponseMetadata) { EXPECT_EQ(4, response->metadataMapsDecodedCount()); // Upstream responds with headers, 100-continue and data. - response = - codec_client_->makeRequestWithBody(Http::TestRequestHeaderMapImpl{{":method", "GET"}, - {":path", "/dynamo/url"}, - {":scheme", "http"}, - {":authority", "host"}, - {"expect", "100-contINUE"}}, - 10); + Http::TestRequestHeaderMapImpl headers = default_request_headers_; + headers.addCopy("expect", "100-contINUE"); + response = codec_client_->makeRequestWithBody(headers, 10); waitForNextUpstreamRequest(); upstream_request_->encode1xxHeaders(Http::TestResponseHeaderMapImpl{{":status", "100"}}); @@ -609,8 +624,17 @@ TEST_P(Http2MetadataIntegrationTest, TestResponseMetadata) { expected_metadata_keys.erase("100-continue"); expected_metadata_keys.insert("aaa"); expected_metadata_keys.insert("keep"); + if (upstreamProtocol() == Http::CodecType::HTTP3) { + // HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter + // adding the "data" metadata header. + expected_metadata_keys.insert("data"); + } verifyExpectedMetadata(response->metadataMap(), expected_metadata_keys); - EXPECT_EQ(2, response->metadataMapsDecodedCount()); + if (upstreamProtocol() == Http::CodecType::HTTP3) { + EXPECT_EQ(3, response->metadataMapsDecodedCount()); + } else { + EXPECT_EQ(2, response->metadataMapsDecodedCount()); + } // Upstream responds with headers, data and metadata that will be consumed. response = codec_client_->makeRequestWithBody(default_request_headers_, 10); @@ -633,7 +657,7 @@ TEST_P(Http2MetadataIntegrationTest, TestResponseMetadata) { EXPECT_EQ(3, response->metadataMapsDecodedCount()); } -TEST_P(Http2MetadataIntegrationTest, ProxyMultipleMetadataReachSizeLimit) { +TEST_P(MetadataIntegrationTest, ProxyMultipleMetadataReachSizeLimit) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -659,7 +683,7 @@ TEST_P(Http2MetadataIntegrationTest, ProxyMultipleMetadataReachSizeLimit) { } // Verifies small metadata can be sent at different locations of a request. -TEST_P(Http2MetadataIntegrationTest, ProxySmallMetadataInRequest) { +TEST_P(MetadataIntegrationTest, ProxySmallMetadataInRequest) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -688,7 +712,7 @@ TEST_P(Http2MetadataIntegrationTest, ProxySmallMetadataInRequest) { } // Verifies large metadata can be sent at different locations of a request. -TEST_P(Http2MetadataIntegrationTest, ProxyLargeMetadataInRequest) { +TEST_P(MetadataIntegrationTest, ProxyLargeMetadataInRequest) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -717,7 +741,7 @@ TEST_P(Http2MetadataIntegrationTest, ProxyLargeMetadataInRequest) { ASSERT_TRUE(response->complete()); } -TEST_P(Http2MetadataIntegrationTest, RequestMetadataReachSizeLimit) { +TEST_P(MetadataIntegrationTest, RequestMetadataReachSizeLimit) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -742,7 +766,7 @@ TEST_P(Http2MetadataIntegrationTest, RequestMetadataReachSizeLimit) { ASSERT_FALSE(response->complete()); } -TEST_P(Http2MetadataIntegrationTest, RequestMetadataThenTrailers) { +TEST_P(MetadataIntegrationTest, RequestMetadataThenTrailers) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -765,7 +789,7 @@ static std::string request_metadata_filter = R"EOF( name: request-metadata-filter )EOF"; -TEST_P(Http2MetadataIntegrationTest, ConsumeAndInsertRequestMetadata) { +TEST_P(MetadataIntegrationTest, ConsumeAndInsertRequestMetadata) { prependFilters({request_metadata_filter}); config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -784,6 +808,11 @@ TEST_P(Http2MetadataIntegrationTest, ConsumeAndInsertRequestMetadata) { // Verifies a headers metadata added. std::set expected_metadata_keys = {"headers"}; expected_metadata_keys.insert("metadata"); + if (downstreamProtocol() == Http::CodecType::HTTP3) { + // HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter + // adding the "data" metadata header. + expected_metadata_keys.insert("data"); + } verifyExpectedMetadata(upstream_request_->metadataMap(), expected_metadata_keys); // Sends a headers only request with metadata. An empty data frame carries end_stream. @@ -869,7 +898,7 @@ TEST_P(Http2MetadataIntegrationTest, ConsumeAndInsertRequestMetadata) { EXPECT_EQ(upstream_request_->duplicatedMetadataKeyCount().find("metadata")->second, 6); } -void Http2MetadataIntegrationTest::runHeaderOnlyTest(bool send_request_body, size_t body_size) { +void MetadataIntegrationTest::runHeaderOnlyTest(bool send_request_body, size_t body_size) { config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { hcm.set_proxy_100_continue(true); }); @@ -880,18 +909,9 @@ void Http2MetadataIntegrationTest::runHeaderOnlyTest(bool send_request_body, siz // Sends a request with body. Only headers will pass through filters. IntegrationStreamDecoderPtr response; if (send_request_body) { - response = codec_client_->makeRequestWithBody( - Http::TestRequestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}, - body_size); + response = codec_client_->makeRequestWithBody(default_request_headers_, body_size); } else { - response = codec_client_->makeHeaderOnlyRequest( - Http::TestRequestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}); + response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); } waitForNextUpstreamRequest(); @@ -900,10 +920,15 @@ void Http2MetadataIntegrationTest::runHeaderOnlyTest(bool send_request_body, siz ASSERT_TRUE(response->complete()); } -void Http2MetadataIntegrationTest::verifyHeadersOnlyTest() { +void MetadataIntegrationTest::verifyHeadersOnlyTest() { // Verifies a headers metadata added. std::set expected_metadata_keys = {"headers"}; expected_metadata_keys.insert("metadata"); + if (downstreamProtocol() == Http::CodecType::HTTP3) { + // HTTP/3 Sends "end stream" in an empty DATA frame which results in the test filter + // adding the "data" metadata header. + expected_metadata_keys.insert("data"); + } verifyExpectedMetadata(upstream_request_->metadataMap(), expected_metadata_keys); // Verifies zero length data received, and end_stream is true. @@ -912,14 +937,14 @@ void Http2MetadataIntegrationTest::verifyHeadersOnlyTest() { EXPECT_EQ(true, upstream_request_->complete()); } -TEST_P(Http2MetadataIntegrationTest, HeadersOnlyRequestWithRequestMetadata) { +TEST_P(MetadataIntegrationTest, HeadersOnlyRequestWithRequestMetadata) { prependFilters({request_metadata_filter}); // Send a headers only request. runHeaderOnlyTest(false, 0); verifyHeadersOnlyTest(); } -void Http2MetadataIntegrationTest::testRequestMetadataWithStopAllFilter() { +void MetadataIntegrationTest::testRequestMetadataWithStopAllFilter() { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -953,17 +978,17 @@ static std::string metadata_stop_all_filter = R"EOF( name: metadata-stop-all-filter )EOF"; -TEST_P(Http2MetadataIntegrationTest, RequestMetadataWithStopAllFilterBeforeMetadataFilter) { +TEST_P(MetadataIntegrationTest, RequestMetadataWithStopAllFilterBeforeMetadataFilter) { prependFilters({request_metadata_filter, metadata_stop_all_filter}); testRequestMetadataWithStopAllFilter(); } -TEST_P(Http2MetadataIntegrationTest, RequestMetadataWithStopAllFilterAfterMetadataFilter) { +TEST_P(MetadataIntegrationTest, RequestMetadataWithStopAllFilterAfterMetadataFilter) { prependFilters({metadata_stop_all_filter, request_metadata_filter}); testRequestMetadataWithStopAllFilter(); } -TEST_P(Http2MetadataIntegrationTest, TestAddEncodedMetadata) { +TEST_P(MetadataIntegrationTest, TestAddEncodedMetadata) { config_helper_.prependFilter(R"EOF( name: encode-headers-return-stop-all-filter )EOF"); @@ -1704,9 +1729,10 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, MultiplexedRingHashIntegrationTest, {Http::CodecType::HTTP1})), HttpProtocolIntegrationTest::protocolTestParamsToString); -INSTANTIATE_TEST_SUITE_P(IpVersions, Http2MetadataIntegrationTest, +INSTANTIATE_TEST_SUITE_P(IpVersions, MetadataIntegrationTest, testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( - {Http::CodecType::HTTP2}, {Http::CodecType::HTTP2})), + {Http::CodecType::HTTP2, Http::CodecType::HTTP3}, + {Http::CodecType::HTTP2, Http::CodecType::HTTP3})), HttpProtocolIntegrationTest::protocolTestParamsToString); void MultiplexedRingHashIntegrationTest::sendMultipleRequests( @@ -2700,7 +2726,11 @@ TEST_P(Http2FrameIntegrationTest, DownstreamSendingEmptyMetadata) { } // Tests that an empty metadata map from upstream is ignored. -TEST_P(Http2MetadataIntegrationTest, UpstreamSendingEmptyMetadata) { +TEST_P(MetadataIntegrationTest, UpstreamSendingEmptyMetadata) { + if (upstreamProtocol() == Http::CodecType::HTTP3) { + // rawWriteConnection is not available for QUIC. + return; + } initialize(); // Send a request and make sure an upstream connection is established. @@ -2728,7 +2758,7 @@ TEST_P(Http2MetadataIntegrationTest, UpstreamSendingEmptyMetadata) { } // Tests upstream sending a metadata frame after ending a stream. -TEST_P(Http2MetadataIntegrationTest, UpstreamMetadataAfterEndStream) { +TEST_P(MetadataIntegrationTest, UpstreamMetadataAfterEndStream) { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -2758,6 +2788,7 @@ TEST_P(Http2MetadataIntegrationTest, UpstreamMetadataAfterEndStream) { ASSERT_TRUE(fake_upstream_connection_->close()); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); + cleanupUpstreamAndDownstream(); } TEST_P(MultiplexedIntegrationTest, InvalidTrailers) {