diff --git a/test/integration/BUILD b/test/integration/BUILD index 7dacd21b4b7c0..86959cfc83b43 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -158,6 +158,25 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "protocol_integration_test", + srcs = [ + "protocol_integration_test.cc", + ], + # As this test has many H1/H2/v4/v6 tests it takes a while to run. + # Shard it enough to bring the run time in line with other integration tests. + shard_count = 3, + deps = [ + ":http_protocol_integration_lib", + "//source/common/http:header_map_lib", + "//source/extensions/filters/http/buffer:config", + "//source/extensions/filters/http/dynamo:config", + "//source/extensions/filters/http/health_check:config", + "//test/integration/filters:random_pause_filter_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "http2_upstream_integration_test", srcs = [ @@ -244,6 +263,9 @@ envoy_cc_test_library( envoy_cc_test( name = "idle_timeout_integration_test", srcs = ["idle_timeout_integration_test.cc"], + # As this test has many pauses for idle timeouts, it takes a while to run. + # Shard it enough to bring the run time in line with other integration tests. + shard_count = 2, deps = [ ":http_protocol_integration_lib", "//test/test_common:test_time_lib", diff --git a/test/integration/http2_integration_test.cc b/test/integration/http2_integration_test.cc index 666abf707efc7..2804776e5d09f 100644 --- a/test/integration/http2_integration_test.cc +++ b/test/integration/http2_integration_test.cc @@ -9,9 +9,10 @@ #include "test/mocks/http/mocks.h" #include "test/test_common/network_utility.h" #include "test/test_common/printers.h" -#include "test/test_common/test_base.h" #include "test/test_common/utility.h" +#include "gtest/gtest.h" + using ::testing::HasSubstr; using ::testing::MatchesRegex; @@ -21,28 +22,6 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, Http2IntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); -TEST_P(Http2IntegrationTest, RouterNotFound) { testRouterNotFound(); } - -TEST_P(Http2IntegrationTest, RouterNotFoundBodyNoBuffer) { testRouterNotFoundWithBody(); } - -TEST_P(Http2IntegrationTest, RouterClusterNotFound404) { testRouterClusterNotFound404(); } - -TEST_P(Http2IntegrationTest, RouterClusterNotFound503) { testRouterClusterNotFound503(); } - -TEST_P(Http2IntegrationTest, RouterRedirect) { testRouterRedirect(); } - -TEST_P(Http2IntegrationTest, ValidZeroLengthContent) { testValidZeroLengthContent(); } - -TEST_P(Http2IntegrationTest, InvalidContentLength) { testInvalidContentLength(); } - -TEST_P(Http2IntegrationTest, MultipleContentLengths) { testMultipleContentLengths(); } - -TEST_P(Http2IntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } - -TEST_P(Http2IntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } - -TEST_P(Http2IntegrationTest, DrainClose) { testDrainClose(); } - TEST_P(Http2IntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { testRouterRequestAndResponseWithBody(1024, 512, false); } @@ -60,10 +39,6 @@ TEST_P(Http2IntegrationTest, RouterRequestAndResponseLargeHeaderNoBuffer) { testRouterRequestAndResponseWithBody(1024, 512, true); } -TEST_P(Http2IntegrationTest, ShutdownWithActiveConnPoolConnections) { - testRequestAndResponseShutdownWithActiveConnection(); -} - TEST_P(Http2IntegrationTest, RouterUpstreamDisconnectBeforeRequestcomplete) { testRouterUpstreamDisconnectBeforeRequestComplete(); } @@ -84,16 +59,10 @@ TEST_P(Http2IntegrationTest, RouterUpstreamResponseBeforeRequestComplete) { testRouterUpstreamResponseBeforeRequestComplete(); } -TEST_P(Http2IntegrationTest, TwoRequests) { testTwoRequests(); } - -TEST_P(Http2IntegrationTest, TwoRequestsWithForcedBackup) { testTwoRequests(true); } - TEST_P(Http2IntegrationTest, Retry) { testRetry(); } TEST_P(Http2IntegrationTest, RetryAttemptCount) { testRetryAttemptCountHeader(); } -TEST_P(Http2IntegrationTest, EnvoyHandling100Continue) { testEnvoyHandling100Continue(); } - static std::string response_metadata_filter = R"EOF( name: response-metadata-filter config: {} @@ -445,41 +414,25 @@ TEST_P(Http2MetadataIntegrationTest, ProxyMultipleMetadataReachSizeLimit) { ASSERT_FALSE(response->complete()); } -TEST_P(Http2IntegrationTest, EnvoyHandlingDuplicate100Continue) { - testEnvoyHandling100Continue(true); -} - -TEST_P(Http2IntegrationTest, EnvoyProxyingEarly100Continue) { testEnvoyProxying100Continue(true); } - -TEST_P(Http2IntegrationTest, EnvoyProxyingLate100Continue) { testEnvoyProxying100Continue(false); } - -TEST_P(Http2IntegrationTest, RetryHittingBufferLimit) { testRetryHittingBufferLimit(); } - -TEST_P(Http2IntegrationTest, HittingDecoderFilterLimit) { testHittingDecoderFilterLimit(); } - -TEST_P(Http2IntegrationTest, HittingEncoderFilterLimit) { testHittingEncoderFilterLimit(); } - -TEST_P(Http2IntegrationTest, GrpcRouterNotFound) { testGrpcRouterNotFound(); } - -TEST_P(Http2IntegrationTest, RetryHostPredicateFilter) { testRetryHostPredicateFilter(); } +TEST_P(Http2IntegrationTest, GrpcRouterNotFound) { + config_helper_.setDefaultHostAndRoute("foo.com", "/found"); + initialize(); -TEST_P(Http2IntegrationTest, RetryPriority) { testRetryPriority(); } + BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( + lookupPort("http"), "POST", "/service/notfound", "", downstream_protocol_, version_, "host", + Http::Headers::get().ContentTypeValues.Grpc); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.Grpc, + response->headers().ContentType()->value().c_str()); + EXPECT_STREQ("12", response->headers().GrpcStatus()->value().c_str()); +} TEST_P(Http2IntegrationTest, GrpcRetry) { testGrpcRetry(); } -TEST_P(Http2IntegrationTest, LargeHeadersInvokeResetStream) { testLargeRequestHeaders(63, 60); } - -TEST_P(Http2IntegrationTest, LargeHeadersAcceptedIfConfigured) { testLargeRequestHeaders(63, 64); } - -TEST_P(Http2IntegrationTest, EncodingHeaderOnlyResponse) { testHeadersOnlyFilterEncoding(); } - -TEST_P(Http2IntegrationTest, DecodingHeaderOnlyResponse) { testHeadersOnlyFilterDecoding(); } +TEST_P(Http2IntegrationTest, LargeHeadersInvokeResetStream) { testLargeRequestHeaders(62, 60); } -TEST_P(Http2IntegrationTest, DecodingHeaderOnlyInterleaved) { testHeadersOnlyFilterInterleaved(); } - -TEST_P(Http2IntegrationTest, DownstreamResetBeforeResponseComplete) { - testDownstreamResetBeforeResponseComplete(); -} +TEST_P(Http2IntegrationTest, LargeHeadersAcceptedIfConfigured) { testLargeRequestHeaders(62, 63); } TEST_P(Http2IntegrationTest, BadMagic) { initialize(); @@ -568,14 +521,6 @@ TEST_P(Http2IntegrationTest, GrpcRequestTimeout) { EXPECT_LT(0, test_server_->counter("cluster.cluster_0.upstream_rq_timeout")->value()); } -// Tests idle timeout behaviour with single request and validates that idle timer kicks in -// after given timeout. -TEST_P(Http2IntegrationTest, IdleTimoutBasic) { testIdleTimeoutBasic(); } - -// Tests idle timeout behaviour with multiple requests and validates that idle timer kicks in -// after both the requests are done. -TEST_P(Http2IntegrationTest, IdleTimeoutWithTwoRequests) { testIdleTimeoutWithTwoRequests(); } - // Interleave two requests and responses and make sure that idle timeout is handled correctly. TEST_P(Http2IntegrationTest, IdleTimeoutWithSimultaneousRequests) { FakeHttpConnectionPtr fake_upstream_connection1; diff --git a/test/integration/http2_upstream_integration_test.cc b/test/integration/http2_upstream_integration_test.cc index 155dd9f0386a2..05c5520ce2244 100644 --- a/test/integration/http2_upstream_integration_test.cc +++ b/test/integration/http2_upstream_integration_test.cc @@ -15,16 +15,6 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, Http2UpstreamIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); -TEST_P(Http2UpstreamIntegrationTest, RouterNotFound) { testRouterNotFound(); } - -TEST_P(Http2UpstreamIntegrationTest, RouterRedirect) { testRouterRedirect(); } - -TEST_P(Http2UpstreamIntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } - -TEST_P(Http2UpstreamIntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } - -TEST_P(Http2UpstreamIntegrationTest, DrainClose) { testDrainClose(); } - TEST_P(Http2UpstreamIntegrationTest, RouterRequestAndResponseWithBodyNoBuffer) { testRouterRequestAndResponseWithBody(1024, 512, false); } @@ -57,32 +47,10 @@ TEST_P(Http2UpstreamIntegrationTest, RouterUpstreamResponseBeforeRequestComplete testRouterUpstreamResponseBeforeRequestComplete(); } -TEST_P(Http2UpstreamIntegrationTest, TwoRequests) { testTwoRequests(); } - TEST_P(Http2UpstreamIntegrationTest, Retry) { testRetry(); } -TEST_P(Http2UpstreamIntegrationTest, EnvoyHandling100Continue) { testEnvoyHandling100Continue(); } - -TEST_P(Http2UpstreamIntegrationTest, EnvoyHandlingDuplicate100Continue) { - testEnvoyHandling100Continue(true); -} - -TEST_P(Http2UpstreamIntegrationTest, EnvoyProxyingEarly100Continue) { - testEnvoyProxying100Continue(true); -} - -TEST_P(Http2UpstreamIntegrationTest, EnvoyProxyingLate100Continue) { - testEnvoyProxying100Continue(false); -} - -TEST_P(Http2UpstreamIntegrationTest, RetryHittingBufferLimit) { testRetryHittingBufferLimit(); } - TEST_P(Http2UpstreamIntegrationTest, GrpcRetry) { testGrpcRetry(); } -TEST_P(Http2UpstreamIntegrationTest, DownstreamResetBeforeResponseComplete) { - testDownstreamResetBeforeResponseComplete(); -} - TEST_P(Http2UpstreamIntegrationTest, Trailers) { testTrailers(1024, 2048); } // Ensure Envoy handles streaming requests and responses simultaneously. diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 931898df1a009..4da6db6e8196a 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -352,14 +352,6 @@ void HttpIntegrationTest::testRouterHeaderOnlyRequestAndResponse( checkSimpleRequestSuccess(0U, 0U, response.get()); } -void HttpIntegrationTest::testRequestAndResponseShutdownWithActiveConnection() { - auto response = makeHeaderOnlyRequest(nullptr, 0); - // Shut down the server with active connection pool connections. - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - test_server_.reset(); - checkSimpleRequestSuccess(0U, 0U, response.get()); -} - // Change the default route to be restrictive, and send a request to an alternate route. void HttpIntegrationTest::testRouterNotFound() { config_helper_.setDefaultHostAndRoute("foo.com", "/found"); @@ -382,111 +374,6 @@ void HttpIntegrationTest::testRouterNotFoundWithBody() { EXPECT_STREQ("404", response->headers().Status()->value().c_str()); } -// Add a route that uses unknown cluster (expect 404 Not Found). -void HttpIntegrationTest::testRouterClusterNotFound404() { - config_helper_.addRoute("foo.com", "/unknown", "unknown_cluster", false, - envoy::api::v2::route::RouteAction::NOT_FOUND, - envoy::api::v2::route::VirtualHost::NONE); - initialize(); - - BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( - lookupPort("http"), "GET", "/unknown", "", downstream_protocol_, version_, "foo.com"); - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("404", response->headers().Status()->value().c_str()); -} - -// Add a route that uses unknown cluster (expect 503 Service Unavailable). -void HttpIntegrationTest::testRouterClusterNotFound503() { - config_helper_.addRoute("foo.com", "/unknown", "unknown_cluster", false, - envoy::api::v2::route::RouteAction::SERVICE_UNAVAILABLE, - envoy::api::v2::route::VirtualHost::NONE); - initialize(); - - BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( - lookupPort("http"), "GET", "/unknown", "", downstream_protocol_, version_, "foo.com"); - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); -} - -// Add a route which redirects HTTP to HTTPS, and verify Envoy sends a 301 -void HttpIntegrationTest::testRouterRedirect() { - config_helper_.addRoute("www.redirect.com", "/", "cluster_0", true, - envoy::api::v2::route::RouteAction::SERVICE_UNAVAILABLE, - envoy::api::v2::route::VirtualHost::ALL); - initialize(); - - BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( - lookupPort("http"), "GET", "/foo", "", downstream_protocol_, version_, "www.redirect.com"); - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("301", response->headers().Status()->value().c_str()); - EXPECT_STREQ("https://www.redirect.com/foo", - response->headers().get(Http::Headers::get().Location)->value().c_str()); -} - -// Add a health check filter and verify correct computation of health based on upstream status. -void HttpIntegrationTest::testComputedHealthCheck() { - config_helper_.addFilter(R"EOF( -name: envoy.health_check -config: - pass_through_mode: false - cluster_min_healthy_percentages: - example_cluster_name: { value: 75 } -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeHeaderOnlyRequest(Http::TestHeaderMapImpl{ - {":method", "GET"}, {":path", "/healthcheck"}, {":scheme", "http"}, {":authority", "host"}}); - response->waitForEndStream(); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); -} - -void HttpIntegrationTest::testAddEncodedTrailers() { - config_helper_.addFilter(R"EOF( -name: add-trailers-filter -config: {} -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 128); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - upstream_request_->encodeData(128, true); - response->waitForEndStream(); - - if (upstreamProtocol() == FakeHttpConnection::Type::HTTP2) { - EXPECT_STREQ("decode", upstream_request_->trailers()->GrpcMessage()->value().c_str()); - } - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { - EXPECT_STREQ("encode", response->trailers()->GrpcMessage()->value().c_str()); - } -} - -// Add a health check filter and verify correct behavior when draining. -void HttpIntegrationTest::testDrainClose() { - config_helper_.addFilter(ConfigHelper::DEFAULT_HEALTH_CHECK_FILTER); - initialize(); - - test_server_->drainManager().draining_ = true; - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); - response->waitForEndStream(); - codec_client_->waitForDisconnect(); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { - EXPECT_TRUE(codec_client_->sawGoAway()); - } - - test_server_->drainManager().draining_ = false; -} - void HttpIntegrationTest::testRouterUpstreamDisconnectBeforeRequestComplete() { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -714,21 +601,6 @@ void HttpIntegrationTest::testRetryAttemptCountHeader() { EXPECT_EQ(512U, response->body().size()); } -// Change the default route to be restrictive, and send a request to an alternate route. -void HttpIntegrationTest::testGrpcRouterNotFound() { - config_helper_.setDefaultHostAndRoute("foo.com", "/found"); - initialize(); - - BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( - lookupPort("http"), "POST", "/service/notfound", "", downstream_protocol_, version_, "host", - Http::Headers::get().ContentTypeValues.Grpc); - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - EXPECT_EQ(Http::Headers::get().ContentTypeValues.Grpc, - response->headers().ContentType()->value().c_str()); - EXPECT_STREQ("12", response->headers().GrpcStatus()->value().c_str()); -} - void HttpIntegrationTest::testGrpcRetry() { Http::TestHeaderMapImpl response_trailers{{"response1", "trailer1"}, {"grpc-status", "0"}}; initialize(); @@ -773,235 +645,6 @@ void HttpIntegrationTest::testGrpcRetry() { } } -// Verifies that a retry priority can be configured and affect the host selected during retries. -// The retry priority will always target P1, which would otherwise never be hit due to P0 being -// healthy. -void HttpIntegrationTest::testRetryPriority() { - const Upstream::HealthyLoad healthy_priority_load({0u, 100u}); - const Upstream::DegradedLoad degraded_priority_load({0u, 100u}); - NiceMock retry_priority(healthy_priority_load, - degraded_priority_load); - Upstream::MockRetryPriorityFactory factory(retry_priority); - - Registry::InjectFactory inject_factory(factory); - - envoy::api::v2::route::RetryPolicy retry_policy; - retry_policy.mutable_retry_priority()->set_name(factory.name()); - - // Add route with custom retry policy - config_helper_.addRoute("host", "/test_retry", "cluster_0", false, - envoy::api::v2::route::RouteAction::NOT_FOUND, - envoy::api::v2::route::VirtualHost::NONE, retry_policy); - - // Use load assignments instead of static hosts. Necessary in order to use priorities. - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - auto cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); - auto load_assignment = cluster->mutable_load_assignment(); - load_assignment->set_cluster_name(cluster->name()); - const auto& host_address = cluster->hosts(0).socket_address().address(); - - for (int i = 0; i < 2; ++i) { - auto locality = load_assignment->add_endpoints(); - locality->set_priority(i); - locality->mutable_locality()->set_region("region"); - locality->mutable_locality()->set_zone("zone"); - locality->mutable_locality()->set_sub_zone("sub_zone" + std::to_string(i)); - auto lb_endpoint = locality->add_lb_endpoints(); - lb_endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_address( - host_address); - lb_endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_port_value( - 0); - } - - cluster->clear_hosts(); - }); - - fake_upstreams_count_ = 2; - initialize(); - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test_retry"}, - {":scheme", "http"}, - {":authority", "host"}, - {"x-forwarded-for", "10.0.0.1"}, - {"x-envoy-retry-on", "5xx"}}, - 1024); - - // Note how we're expecting each upstream request to hit the same upstream. - waitForNextUpstreamRequest(0); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - - if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - ASSERT_TRUE(fake_upstreams_[1]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); - } else { - ASSERT_TRUE(upstream_request_->waitForReset()); - } - - waitForNextUpstreamRequest(1); - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(512, true); - - response->waitForEndStream(); - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_EQ(1024U, upstream_request_->bodyLength()); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - EXPECT_EQ(512U, response->body().size()); -} - -// -// Verifies that a retry host filter can be configured and affect the host selected during retries. -// The predicate will keep track of the first host attempted, and attempt to route all requests to -// the same host. With a total of two upstream hosts, this should result in us continuously sending -// requests to the same host. -void HttpIntegrationTest::testRetryHostPredicateFilter() { - TestHostPredicateFactory predicate_factory; - Registry::InjectFactory inject_factory(predicate_factory); - - envoy::api::v2::route::RetryPolicy retry_policy; - retry_policy.add_retry_host_predicate()->set_name(predicate_factory.name()); - - // Add route with custom retry policy - config_helper_.addRoute("host", "/test_retry", "cluster_0", false, - envoy::api::v2::route::RouteAction::NOT_FOUND, - envoy::api::v2::route::VirtualHost::NONE, retry_policy); - - // We want to work with a cluster with two hosts. - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - auto* new_host = bootstrap.mutable_static_resources()->mutable_clusters(0)->add_hosts(); - new_host->MergeFrom(bootstrap.static_resources().clusters(0).hosts(0)); - }); - fake_upstreams_count_ = 2; - initialize(); - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test_retry"}, - {":scheme", "http"}, - {":authority", "host"}, - {"x-forwarded-for", "10.0.0.1"}, - {"x-envoy-retry-on", "5xx"}}, - 1024); - - // Note how we're expecting each upstream request to hit the same upstream. - auto upstream_idx = waitForNextUpstreamRequest({0, 1}); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - - if (fake_upstreams_[upstream_idx]->httpType() == FakeHttpConnection::Type::HTTP1) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - ASSERT_TRUE(fake_upstreams_[upstream_idx]->waitForHttpConnection(*dispatcher_, - fake_upstream_connection_)); - } else { - ASSERT_TRUE(upstream_request_->waitForReset()); - } - - waitForNextUpstreamRequest(upstream_idx); - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(512, true); - - response->waitForEndStream(); - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_EQ(1024U, upstream_request_->bodyLength()); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - EXPECT_EQ(512U, response->body().size()); -} - -// Very similar set-up to testRetry but with a 16k request the request will not -// be buffered and the 503 will be returned to the user. -void HttpIntegrationTest::testRetryHittingBufferLimit() { - config_helper_.setBufferLimits(1024, 1024); // Set buffer limits upstream and downstream. - initialize(); - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}, - {"x-forwarded-for", "10.0.0.1"}, - {"x-envoy-retry-on", "5xx"}}, - 1024 * 65); - waitForNextUpstreamRequest(); - - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, true); - - response->waitForEndStream(); - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_EQ(66560U, upstream_request_->bodyLength()); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); -} - -// Test hitting the dynamo filter with too many request bytes to buffer. Ensure the connection -// manager sends a 413. -void HttpIntegrationTest::testHittingDecoderFilterLimit() { - config_helper_.addFilter("{ name: envoy.http_dynamo_filter, config: {} }"); - config_helper_.setBufferLimits(1024, 1024); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - // Envoy will likely connect and proxy some unspecified amount of data before - // hitting the buffer limit and disconnecting. Ignore this if it happens. - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/dynamo/url"}, - {":scheme", "http"}, - {":authority", "host"}, - {"x-forwarded-for", "10.0.0.1"}, - {"x-envoy-retry-on", "5xx"}}, - 1024 * 65); - - response->waitForEndStream(); - // With HTTP/1 there's a possible race where if the connection backs up early, - // the 413-and-connection-close may be sent while the body is still being - // sent, resulting in a write error and the connection being closed before the - // response is read. - if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { - ASSERT_TRUE(response->complete()); - } - if (response->complete()) { - EXPECT_STREQ("413", response->headers().Status()->value().c_str()); - } -} - -// Test hitting the dynamo filter with too many response bytes to buffer. Given the request headers -// are sent on early, the stream/connection will be reset. -void HttpIntegrationTest::testHittingEncoderFilterLimit() { - config_helper_.addFilter("{ name: envoy.http_dynamo_filter, config: {} }"); - config_helper_.setBufferLimits(1024, 1024); - initialize(); - - // Send the request. - codec_client_ = makeHttpConnection(lookupPort("http")); - auto encoder_decoder = codec_client_->startRequest(default_request_headers_); - auto downstream_request = &encoder_decoder.first; - auto response = std::move(encoder_decoder.second); - Buffer::OwnedImpl data("{\"TableName\":\"locations\"}"); - codec_client_->sendData(*downstream_request, data, true); - waitForNextUpstreamRequest(); - - // Send the response headers. - upstream_request_->encodeHeaders(default_response_headers_, false); - - // Now send an overly large response body. At some point, too much data will - // be buffered, the stream will be reset, and the connection will disconnect. - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - upstream_request_->encodeData(1024 * 65, false); - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - - response->waitForEndStream(); - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("500", response->headers().Status()->value().c_str()); -} - void HttpIntegrationTest::testEnvoyHandling100Continue(bool additional_continue_from_upstream, const std::string& via) { initialize(); @@ -1115,137 +758,6 @@ void HttpIntegrationTest::testEnvoyProxying100Continue(bool continue_before_upst EXPECT_STREQ("200", response->headers().Status()->value().c_str()); } -void HttpIntegrationTest::testIdleTimeoutBasic() { - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - auto* static_resources = bootstrap.mutable_static_resources(); - auto* cluster = static_resources->mutable_clusters(0); - auto* http_protocol_options = cluster->mutable_common_http_protocol_options(); - auto* idle_time_out = http_protocol_options->mutable_idle_timeout(); - std::chrono::milliseconds timeout(1000); - auto seconds = std::chrono::duration_cast(timeout); - idle_time_out->set_seconds(seconds.count()); - }); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); - waitForNextUpstreamRequest(); - - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(512, true); - response->waitForEndStream(); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); - - // Do not send any requests and validate if idle time out kicks in. - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_idle_timeout", 1); -} - -void HttpIntegrationTest::testIdleTimeoutWithTwoRequests() { - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - auto* static_resources = bootstrap.mutable_static_resources(); - auto* cluster = static_resources->mutable_clusters(0); - auto* http_protocol_options = cluster->mutable_common_http_protocol_options(); - auto* idle_time_out = http_protocol_options->mutable_idle_timeout(); - std::chrono::milliseconds timeout(1000); - auto seconds = std::chrono::duration_cast(timeout); - idle_time_out->set_seconds(seconds.count()); - }); - - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - // Request 1. - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); - waitForNextUpstreamRequest(); - - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(512, true); - response->waitForEndStream(); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); - - // Request 2. - response = codec_client_->makeRequestWithBody(default_request_headers_, 512); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(1024, true); - response->waitForEndStream(); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 2); - - // Do not send any requests and validate if idle time out kicks in. - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_idle_timeout", 1); -} - -// This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a -// pending request is not sent on a connection that has been half-closed. -void HttpIntegrationTest::testUpstreamDisconnectWithTwoRequests() { - config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { - auto* static_resources = bootstrap.mutable_static_resources(); - auto* cluster = static_resources->mutable_clusters(0); - // Ensure we only have one connection upstream, one request active at a time. - cluster->mutable_max_requests_per_connection()->set_value(1); - auto* circuit_breakers = cluster->mutable_circuit_breakers(); - circuit_breakers->add_thresholds()->mutable_max_connections()->set_value(1); - }); - initialize(); - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - // Request 1. - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); - waitForNextUpstreamRequest(); - - // Request 2. - IntegrationCodecClientPtr codec_client2 = makeHttpConnection(lookupPort("http")); - auto response2 = codec_client2->makeRequestWithBody(default_request_headers_, 512); - - // Validate one request active, the other pending. - test_server_->waitForGaugeEq("cluster.cluster_0.upstream_rq_active", 1); - test_server_->waitForGaugeEq("cluster.cluster_0.upstream_rq_pending_active", 1); - - // Response 1. - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(512, true); - ASSERT_TRUE(fake_upstream_connection_->close()); - response->waitForEndStream(); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); - - // Response 2. - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - fake_upstream_connection_.reset(); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(default_response_headers_, false); - upstream_request_->encodeData(1024, true); - response2->waitForEndStream(); - codec_client2->close(); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response2->complete()); - EXPECT_STREQ("200", response2->headers().Status()->value().c_str()); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 2); - test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 2); -} - void HttpIntegrationTest::testTwoRequests(bool network_backup) { // if network_backup is false, this simply tests that Envoy can handle multiple // requests on a connection. @@ -1292,76 +804,6 @@ void HttpIntegrationTest::testTwoRequests(bool network_backup) { EXPECT_EQ(1024U, response->body().size()); } -void HttpIntegrationTest::testValidZeroLengthContent() { - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - Http::TestHeaderMapImpl request_headers{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}, - {"content-length", "0"}}; - auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("200", response->headers().Status()->value().c_str()); -} - -void HttpIntegrationTest::testInvalidContentLength() { - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - auto encoder_decoder = - codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":authority", "host"}, - {"content-length", "-1"}}); - auto response = std::move(encoder_decoder.second); - - if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { - codec_client_->waitForDisconnect(); - } else { - response->waitForReset(); - codec_client_->close(); - } - - if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("400", response->headers().Status()->value().c_str()); - } else { - ASSERT_TRUE(response->reset()); - EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->reset_reason()); - } -} - -void HttpIntegrationTest::testMultipleContentLengths() { - initialize(); - codec_client_ = makeHttpConnection(lookupPort("http")); - auto encoder_decoder = - codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":authority", "host"}, - {"content-length", "3,2"}}); - auto response = std::move(encoder_decoder.second); - - if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { - codec_client_->waitForDisconnect(); - } else { - response->waitForReset(); - codec_client_->close(); - } - - if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { - ASSERT_TRUE(response->complete()); - EXPECT_STREQ("400", response->headers().Status()->value().c_str()); - } else { - ASSERT_TRUE(response->reset()); - EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->reset_reason()); - } -} - void HttpIntegrationTest::testLargeRequestHeaders(uint32_t size, uint32_t max_size) { // `size` parameter is the size of the header that will be added to the // request. The actual request byte size will exceed `size` due to keys @@ -1398,160 +840,6 @@ void HttpIntegrationTest::testLargeRequestHeaders(uint32_t size, uint32_t max_si } } -void HttpIntegrationTest::testHeadersOnlyFilterEncoding() { - config_helper_.addFilter(R"EOF( -name: encode-headers-only -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "GET"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}, - 128); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - response->waitForEndStream(); - EXPECT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); - if (upstreamProtocol() == FakeHttpConnection::Type::HTTP1) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - } else { - ASSERT_TRUE(upstream_request_->waitForReset()); - ASSERT_TRUE(fake_upstream_connection_->close()); - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - } - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - EXPECT_EQ(0, response->body().size()); -} - -void HttpIntegrationTest::testHeadersOnlyFilterDecoding() { - config_helper_.addFilter(R"EOF( -name: decode-headers-only -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}, - 128); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - upstream_request_->encodeData(128, true); - response->waitForEndStream(); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - EXPECT_EQ(128, response->body().size()); -} - -void HttpIntegrationTest::testHeadersOnlyFilterEncodingIntermediateFilters() { - config_helper_.addFilter(R"EOF( -name: passthrough-filter -)EOF"); - config_helper_.addFilter(R"EOF( -name: encode-headers-only -)EOF"); - config_helper_.addFilter(R"EOF( -name: passthrough-filter -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "GET"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}, - 128); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - response->waitForEndStream(); - if (upstreamProtocol() == FakeHttpConnection::Type::HTTP1) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - } else { - ASSERT_TRUE(upstream_request_->waitForReset()); - ASSERT_TRUE(fake_upstream_connection_->close()); - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - } - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - EXPECT_EQ(0, response->body().size()); -} - -void HttpIntegrationTest::testHeadersOnlyFilterDecodingIntermediateFilters() { - config_helper_.addFilter(R"EOF( -name: passthrough-filter -)EOF"); - config_helper_.addFilter(R"EOF( -name: decode-headers-only -)EOF"); - config_helper_.addFilter(R"EOF( -name: passthrough-filter -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - auto response = - codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}, - 128); - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - upstream_request_->encodeData(128, true); - response->waitForEndStream(); - - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - EXPECT_EQ(128, response->body().size()); -} - -// Verifies behavior when request data is encoded after the request has been -// turned into a headers-only request and the response has already begun. -void HttpIntegrationTest::testHeadersOnlyFilterInterleaved() { - config_helper_.addFilter(R"EOF( -name: decode-headers-only -)EOF"); - initialize(); - - codec_client_ = makeHttpConnection(lookupPort("http")); - - // First send the request headers. The filter should turn this into a header-only - // request. - auto encoder_decoder = - codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "GET"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", "host"}}); - request_encoder_ = &encoder_decoder.first; - auto response = std::move(encoder_decoder.second); - - // Wait for the upstream request and begin sending a response with end_stream = false. - waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); - - // Simulate additional data after the request has been turned into a headers only request. - Buffer::OwnedImpl data(std::string(128, 'a')); - request_encoder_->encodeData(data, false); - - // End the response. - upstream_request_->encodeData(128, true); - - response->waitForEndStream(); - EXPECT_TRUE(response->complete()); - EXPECT_STREQ("503", response->headers().Status()->value().c_str()); - EXPECT_EQ(0, upstream_request_->body().length()); -} - void HttpIntegrationTest::testDownstreamResetBeforeResponseComplete() { initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -1635,5 +923,4 @@ std::string HttpIntegrationTest::listenerStatPrefix(const std::string& stat_name } return "listener.[__1]_0." + stat_name; } - } // namespace Envoy diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 743b009f0c049..a97c3465f70dd 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -132,18 +132,17 @@ class HttpIntegrationTest : public BaseIntegrationTest { // Sends a simple header-only HTTP request, and waits for a response. IntegrationStreamDecoderPtr makeHeaderOnlyRequest(ConnectionCreationFunction* create_connection, int upstream_index); - - void testRouterRedirect(); void testRouterNotFound(); void testRouterNotFoundWithBody(); - void testRouterClusterNotFound404(); - void testRouterClusterNotFound503(); + void testRouterRequestAndResponseWithBody(uint64_t request_size, uint64_t response_size, bool big_header, ConnectionCreationFunction* creator = nullptr); void testRouterHeaderOnlyRequestAndResponse(ConnectionCreationFunction* creator = nullptr, int upstream_index = 0); void testRequestAndResponseShutdownWithActiveConnection(); + + // Disconnect tests void testRouterUpstreamDisconnectBeforeRequestComplete(); void testRouterUpstreamDisconnectBeforeResponseComplete(ConnectionCreationFunction* creator = nullptr); @@ -152,36 +151,16 @@ class HttpIntegrationTest : public BaseIntegrationTest { void testRouterDownstreamDisconnectBeforeResponseComplete( ConnectionCreationFunction* creator = nullptr); void testRouterUpstreamResponseBeforeRequestComplete(); + void testTwoRequests(bool force_network_backup = false); void testLargeRequestHeaders(uint32_t size, uint32_t max_size = 60); - void testIdleTimeoutBasic(); - void testIdleTimeoutWithTwoRequests(); - void testIdleTimerDisabled(); - void testUpstreamDisconnectWithTwoRequests(); - void testHeadersOnlyFilterEncoding(); - void testHeadersOnlyFilterDecoding(); - void testHeadersOnlyFilterEncodingIntermediateFilters(); - void testHeadersOnlyFilterDecodingIntermediateFilters(); - void testHeadersOnlyFilterInterleaved(); - - // Test that a request returns the same content with both allow_absolute_urls enabled and - // allow_absolute_urls disabled - void testDefaultHost(); - void testValidZeroLengthContent(); - void testInvalidContentLength(); - void testMultipleContentLengths(); - void testComputedHealthCheck(); + void testAddEncodedTrailers(); - void testDrainClose(); void testRetry(); void testRetryHittingBufferLimit(); void testRetryAttemptCountHeader(); - void testGrpcRouterNotFound(); void testGrpcRetry(); - void testRetryPriority(); - void testRetryHostPredicateFilter(); - void testHittingDecoderFilterLimit(); - void testHittingEncoderFilterLimit(); + void testEnvoyHandling100Continue(bool additional_continue_from_upstream = false, const std::string& via = ""); void testEnvoyProxying100Continue(bool continue_before_upstream_complete = false, diff --git a/test/integration/idle_timeout_integration_test.cc b/test/integration/idle_timeout_integration_test.cc index 299c6582f28a2..edaa2e363cc83 100644 --- a/test/integration/idle_timeout_integration_test.cc +++ b/test/integration/idle_timeout_integration_test.cc @@ -82,6 +82,85 @@ INSTANTIATE_TEST_SUITE_P(Protocols, IdleTimeoutIntegrationTest, testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), HttpProtocolIntegrationTest::protocolTestParamsToString); +// Tests idle timeout behaviour with single request and validates that idle timer kicks in +// after given timeout. +TEST_P(IdleTimeoutIntegrationTest, TimeoutBasic) { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* static_resources = bootstrap.mutable_static_resources(); + auto* cluster = static_resources->mutable_clusters(0); + auto* http_protocol_options = cluster->mutable_common_http_protocol_options(); + auto* idle_time_out = http_protocol_options->mutable_idle_timeout(); + std::chrono::milliseconds timeout(1000); + auto seconds = std::chrono::duration_cast(timeout); + idle_time_out->set_seconds(seconds.count()); + }); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + response->waitForEndStream(); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); + + // Do not send any requests and validate if idle time out kicks in. + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_idle_timeout", 1); +} + +// Tests idle timeout behaviour with multiple requests and validates that idle timer kicks in +// after both the requests are done. +TEST_P(IdleTimeoutIntegrationTest, IdleTimeoutWithTwoRequests) { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* static_resources = bootstrap.mutable_static_resources(); + auto* cluster = static_resources->mutable_clusters(0); + auto* http_protocol_options = cluster->mutable_common_http_protocol_options(); + auto* idle_time_out = http_protocol_options->mutable_idle_timeout(); + std::chrono::milliseconds timeout(1000); + auto seconds = std::chrono::duration_cast(timeout); + idle_time_out->set_seconds(seconds.count()); + }); + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Request 1. + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + response->waitForEndStream(); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); + + // Request 2. + response = codec_client_->makeRequestWithBody(default_request_headers_, 512); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(1024, true); + response->waitForEndStream(); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 2); + + // Do not send any requests and validate if idle time out kicks in. + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_idle_timeout", 1); +} + // Per-stream idle timeout after having sent downstream headers. TEST_P(IdleTimeoutIntegrationTest, PerStreamIdleTimeoutAfterDownstreamHeaders) { enable_per_stream_idle_timeout_ = true; diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index 8b2be331f9f68..f262ab2b6703d 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -52,16 +52,6 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, IntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); -TEST_P(IntegrationTest, RouterNotFound) { testRouterNotFound(); } - -TEST_P(IntegrationTest, RouterNotFoundBodyNoBuffer) { testRouterNotFoundWithBody(); } - -TEST_P(IntegrationTest, RouterClusterNotFound404) { testRouterClusterNotFound404(); } - -TEST_P(IntegrationTest, RouterClusterNotFound503) { testRouterClusterNotFound503(); } - -TEST_P(IntegrationTest, RouterRedirect) { testRouterRedirect(); } - TEST_P(IntegrationTest, RouterDirectResponse) { const std::string body = "Response body"; const std::string file_path = TestEnvironment::writeStringToFileForTest("test_envoy", body); @@ -103,12 +93,6 @@ TEST_P(IntegrationTest, RouterDirectResponse) { EXPECT_EQ(body, response->body()); } -TEST_P(IntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } - -TEST_P(IntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } - -TEST_P(IntegrationTest, DrainClose) { testDrainClose(); } - TEST_P(IntegrationTest, ConnectionClose) { config_helper_.addFilter(ConfigHelper::DEFAULT_HEALTH_CHECK_FILTER); initialize(); @@ -143,10 +127,6 @@ TEST_P(IntegrationTest, RouterHeaderOnlyRequestAndResponseNoBuffer) { testRouterHeaderOnlyRequestAndResponse(); } -TEST_P(IntegrationTest, ShutdownWithActiveConnPoolConnections) { - testRequestAndResponseShutdownWithActiveConnection(); -} - TEST_P(IntegrationTest, RouterUpstreamDisconnectBeforeRequestcomplete) { testRouterUpstreamDisconnectBeforeRequestComplete(); } @@ -167,22 +147,6 @@ TEST_P(IntegrationTest, RouterUpstreamResponseBeforeRequestComplete) { testRouterUpstreamResponseBeforeRequestComplete(); } -TEST_P(IntegrationTest, Retry) { testRetry(); } - -TEST_P(IntegrationTest, RetryAttemptCount) { testRetryAttemptCountHeader(); } - -TEST_P(IntegrationTest, RetryHostPredicateFilter) { testRetryHostPredicateFilter(); } - -TEST_P(IntegrationTest, RetryPriority) { testRetryPriority(); } - -TEST_P(IntegrationTest, EnvoyHandling100Continue) { testEnvoyHandling100Continue(); } - -TEST_P(IntegrationTest, EnvoyHandlingDuplicate100Continues) { testEnvoyHandling100Continue(true); } - -TEST_P(IntegrationTest, EnvoyProxyingEarly100Continue) { testEnvoyProxying100Continue(true); } - -TEST_P(IntegrationTest, EnvoyProxyingLate100Continue) { testEnvoyProxying100Continue(false); } - TEST_P(IntegrationTest, EnvoyProxyingEarly100ContinueWithEncoderFilter) { testEnvoyProxying100Continue(true, true); } @@ -191,39 +155,61 @@ TEST_P(IntegrationTest, EnvoyProxyingLate100ContinueWithEncoderFilter) { testEnvoyProxying100Continue(false, true); } -TEST_P(IntegrationTest, TwoRequests) { testTwoRequests(); } - -TEST_P(IntegrationTest, TwoRequestsWithForcedBackup) { testTwoRequests(true); } - +// This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a +// pending request is not sent on a connection that has been half-closed. TEST_P(IntegrationTest, UpstreamDisconnectWithTwoRequests) { - testUpstreamDisconnectWithTwoRequests(); -} - -TEST_P(IntegrationTest, EncodingHeaderOnlyResponse) { testHeadersOnlyFilterEncoding(); } + config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* static_resources = bootstrap.mutable_static_resources(); + auto* cluster = static_resources->mutable_clusters(0); + // Ensure we only have one connection upstream, one request active at a time. + cluster->mutable_max_requests_per_connection()->set_value(1); + auto* circuit_breakers = cluster->mutable_circuit_breakers(); + circuit_breakers->add_thresholds()->mutable_max_connections()->set_value(1); + }); + initialize(); + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); -TEST_P(IntegrationTest, DecodingHeaderOnlyResponse) { testHeadersOnlyFilterDecoding(); } + codec_client_ = makeHttpConnection(lookupPort("http")); -TEST_P(IntegrationTest, EncodingHeaderOnlyResponseIntermediateFilters) { - testHeadersOnlyFilterEncodingIntermediateFilters(); -} + // Request 1. + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + waitForNextUpstreamRequest(); -TEST_P(IntegrationTest, DecodingHeaderOnlyResponseIntermediateFilters) { - testHeadersOnlyFilterDecodingIntermediateFilters(); -} + // Request 2. + IntegrationCodecClientPtr codec_client2 = makeHttpConnection(lookupPort("http")); + auto response2 = codec_client2->makeRequestWithBody(default_request_headers_, 512); -TEST_P(IntegrationTest, DecodingHeaderOnlyInterleaved) { testHeadersOnlyFilterInterleaved(); } + // Validate one request active, the other pending. + test_server_->waitForGaugeEq("cluster.cluster_0.upstream_rq_active", 1); + test_server_->waitForGaugeEq("cluster.cluster_0.upstream_rq_pending_active", 1); -TEST_P(IntegrationTest, RetryHittingBufferLimit) { testRetryHittingBufferLimit(); } + // Response 1. + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + ASSERT_TRUE(fake_upstream_connection_->close()); + response->waitForEndStream(); -TEST_P(IntegrationTest, HittingDecoderFilterLimit) { testHittingDecoderFilterLimit(); } + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 1); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 1); -// Tests idle timeout behaviour with single request and validates that idle timer kicks in -// after given timeout. -TEST_P(IntegrationTest, IdleTimoutBasic) { testIdleTimeoutBasic(); } + // Response 2. + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + fake_upstream_connection_.reset(); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(1024, true); + response2->waitForEndStream(); + codec_client2->close(); -// Tests idle timeout behaviour with multiple requests and validates that idle timer kicks in -// after both the requests are done. -TEST_P(IntegrationTest, IdleTimeoutWithTwoRequests) { testIdleTimeoutWithTwoRequests(); } + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response2->complete()); + EXPECT_STREQ("200", response2->headers().Status()->value().c_str()); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_cx_total", 2); + test_server_->waitForCounterGe("cluster.cluster_0.upstream_rq_200", 2); +} // Test hitting the bridge filter with too many response bytes to buffer. Given // the headers are not proxied, the connection manager will send a local error reply. @@ -257,8 +243,6 @@ TEST_P(IntegrationTest, HittingGrpcFilterLimitBufferingHeaders) { HeaderValueOf(Headers::get().GrpcStatus, "2")); // Unknown gRPC error } -TEST_P(IntegrationTest, HittingEncoderFilterLimit) { testHittingEncoderFilterLimit(); } - TEST_P(IntegrationTest, BadFirstline) { initialize(); std::string response; @@ -484,15 +468,9 @@ TEST_P(IntegrationTest, Connect) { EXPECT_EQ(normalizeDate(response1), normalizeDate(response2)); } -TEST_P(IntegrationTest, ValidZeroLengthContent) { testValidZeroLengthContent(); } - -TEST_P(IntegrationTest, InvalidContentLength) { testInvalidContentLength(); } - -TEST_P(IntegrationTest, MultipleContentLengths) { testMultipleContentLengths(); } - -TEST_P(IntegrationTest, LargeHeadersRejected) { testLargeRequestHeaders(63, 60); } +TEST_P(IntegrationTest, LargeHeadersRejected) { testLargeRequestHeaders(62, 60); } -TEST_P(IntegrationTest, LargeHeadersAcceptedIfConfigured) { testLargeRequestHeaders(63, 64); } +TEST_P(IntegrationTest, LargeHeadersAccepted) { testLargeRequestHeaders(62, 63); } TEST_P(IntegrationTest, UpstreamProtocolError) { initialize(); diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc new file mode 100644 index 0000000000000..56273d07a2d0c --- /dev/null +++ b/test/integration/protocol_integration_test.cc @@ -0,0 +1,734 @@ +#include +#include +#include +#include +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/event/dispatcher.h" +#include "envoy/http/header_map.h" +#include "envoy/registry/registry.h" + +#include "common/api/api_impl.h" +#include "common/buffer/buffer_impl.h" +#include "common/common/fmt.h" +#include "common/common/thread_annotations.h" +#include "common/http/headers.h" +#include "common/network/utility.h" +#include "common/protobuf/utility.h" +#include "common/runtime/runtime_impl.h" +#include "common/upstream/upstream_impl.h" + +#include "test/common/upstream/utility.h" +#include "test/integration/autonomous_upstream.h" +#include "test/integration/http_integration.h" +#include "test/integration/http_protocol_integration.h" +#include "test/integration/test_host_predicate_config.h" +#include "test/integration/utility.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/network_utility.h" +#include "test/test_common/registry.h" + +#include "gtest/gtest.h" + +using testing::_; +using testing::AnyNumber; +using testing::HasSubstr; +using testing::Invoke; +using testing::Not; + +namespace Envoy { + +// Tests for DownstreamProtocolIntegrationTest will be run with all protocols +// (H1/H2 downstream) but only H1 upstreams. +// +// This is useful for things which will likely not differ based on upstream +// behavior, for example "how does Envoy handle duplicate content lengths from +// downstream"? +typedef HttpProtocolIntegrationTest DownstreamProtocolIntegrationTest; + +// Tests for ProtocolIntegrationTest will be run with the full mesh of H1/H2 +// downstream and H1/H2 upstreams. +typedef HttpProtocolIntegrationTest ProtocolIntegrationTest; + +TEST_P(ProtocolIntegrationTest, ShutdownWithActiveConnPoolConnections) { + auto response = makeHeaderOnlyRequest(nullptr, 0); + // Shut down the server with active connection pool connections. + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); + test_server_.reset(); + checkSimpleRequestSuccess(0U, 0U, response.get()); +} + +// Change the default route to be restrictive, and send a request to an alternate route. +TEST_P(ProtocolIntegrationTest, RouterNotFound) { testRouterNotFound(); } + +// Change the default route to be restrictive, and send a POST to an alternate route. +TEST_P(DownstreamProtocolIntegrationTest, RouterNotFoundBodyNoBuffer) { + testRouterNotFoundWithBody(); +} + +// Add a route that uses unknown cluster (expect 404 Not Found). +TEST_P(DownstreamProtocolIntegrationTest, RouterClusterNotFound404) { + config_helper_.addRoute("foo.com", "/unknown", "unknown_cluster", false, + envoy::api::v2::route::RouteAction::NOT_FOUND, + envoy::api::v2::route::VirtualHost::NONE); + initialize(); + + BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( + lookupPort("http"), "GET", "/unknown", "", downstream_protocol_, version_, "foo.com"); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("404", response->headers().Status()->value().c_str()); +} + +// Add a route that uses unknown cluster (expect 503 Service Unavailable). +TEST_P(DownstreamProtocolIntegrationTest, RouterClusterNotFound503) { + config_helper_.addRoute("foo.com", "/unknown", "unknown_cluster", false, + envoy::api::v2::route::RouteAction::SERVICE_UNAVAILABLE, + envoy::api::v2::route::VirtualHost::NONE); + initialize(); + + BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( + lookupPort("http"), "GET", "/unknown", "", downstream_protocol_, version_, "foo.com"); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); +} + +// Add a route which redirects HTTP to HTTPS, and verify Envoy sends a 301 +TEST_P(ProtocolIntegrationTest, RouterRedirect) { + config_helper_.addRoute("www.redirect.com", "/", "cluster_0", true, + envoy::api::v2::route::RouteAction::SERVICE_UNAVAILABLE, + envoy::api::v2::route::VirtualHost::ALL); + initialize(); + + BufferingStreamDecoderPtr response = IntegrationUtil::makeSingleRequest( + lookupPort("http"), "GET", "/foo", "", downstream_protocol_, version_, "www.redirect.com"); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("301", response->headers().Status()->value().c_str()); + EXPECT_STREQ("https://www.redirect.com/foo", + response->headers().get(Http::Headers::get().Location)->value().c_str()); +} + +// Add a health check filter and verify correct computation of health based on upstream status. +TEST_P(ProtocolIntegrationTest, ComputedHealthCheck) { + config_helper_.addFilter(R"EOF( +name: envoy.health_check +config: + pass_through_mode: false + cluster_min_healthy_percentages: + example_cluster_name: { value: 75 } +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestHeaderMapImpl{ + {":method", "GET"}, {":path", "/healthcheck"}, {":scheme", "http"}, {":authority", "host"}}); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); +} + +TEST_P(ProtocolIntegrationTest, AddEncodedTrailers) { + config_helper_.addFilter(R"EOF( +name: add-trailers-filter +config: {} +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + upstream_request_->encodeData(128, true); + response->waitForEndStream(); + + if (upstreamProtocol() == FakeHttpConnection::Type::HTTP2) { + EXPECT_STREQ("decode", upstream_request_->trailers()->GrpcMessage()->value().c_str()); + } + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { + EXPECT_STREQ("encode", response->trailers()->GrpcMessage()->value().c_str()); + } +} + +// Add a health check filter and verify correct behavior when draining. +TEST_P(ProtocolIntegrationTest, DrainClose) { + config_helper_.addFilter(ConfigHelper::DEFAULT_HEALTH_CHECK_FILTER); + initialize(); + + test_server_->drainManager().draining_ = true; + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + response->waitForEndStream(); + codec_client_->waitForDisconnect(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { + EXPECT_TRUE(codec_client_->sawGoAway()); + } + + test_server_->drainManager().draining_ = false; +} + +TEST_P(ProtocolIntegrationTest, Retry) { + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + + if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(1024U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + EXPECT_EQ(512U, response->body().size()); +} + +// Tests that the x-envoy-attempt-count header is properly set on the upstream request +// and updated after the request is retried. +TEST_P(DownstreamProtocolIntegrationTest, RetryAttemptCountHeader) { + config_helper_.addRoute("host", "/test_retry", "cluster_0", false, + envoy::api::v2::route::RouteAction::NOT_FOUND, + envoy::api::v2::route::VirtualHost::NONE, {}, true); + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test_retry"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + + EXPECT_EQ(atoi(upstream_request_->headers().EnvoyAttemptCount()->value().c_str()), 1); + + if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + waitForNextUpstreamRequest(); + EXPECT_EQ(atoi(upstream_request_->headers().EnvoyAttemptCount()->value().c_str()), 2); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(1024U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + EXPECT_EQ(512U, response->body().size()); +} + +// Verifies that a retry priority can be configured and affect the host selected during retries. +// The retry priority will always target P1, which would otherwise never be hit due to P0 being +// healthy. +TEST_P(DownstreamProtocolIntegrationTest, RetryPriority) { + const Upstream::HealthyLoad healthy_priority_load({0u, 100u}); + const Upstream::DegradedLoad degraded_priority_load({0u, 100u}); + NiceMock retry_priority(healthy_priority_load, + degraded_priority_load); + Upstream::MockRetryPriorityFactory factory(retry_priority); + + Registry::InjectFactory inject_factory(factory); + + envoy::api::v2::route::RetryPolicy retry_policy; + retry_policy.mutable_retry_priority()->set_name(factory.name()); + + // Add route with custom retry policy + config_helper_.addRoute("host", "/test_retry", "cluster_0", false, + envoy::api::v2::route::RouteAction::NOT_FOUND, + envoy::api::v2::route::VirtualHost::NONE, retry_policy); + + // Use load assignments instead of static hosts. Necessary in order to use priorities. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + auto load_assignment = cluster->mutable_load_assignment(); + load_assignment->set_cluster_name(cluster->name()); + const auto& host_address = cluster->hosts(0).socket_address().address(); + + for (int i = 0; i < 2; ++i) { + auto locality = load_assignment->add_endpoints(); + locality->set_priority(i); + locality->mutable_locality()->set_region("region"); + locality->mutable_locality()->set_zone("zone"); + locality->mutable_locality()->set_sub_zone("sub_zone" + std::to_string(i)); + auto lb_endpoint = locality->add_lb_endpoints(); + lb_endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_address( + host_address); + lb_endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_port_value( + 0); + } + + cluster->clear_hosts(); + }); + + fake_upstreams_count_ = 2; + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test_retry"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024); + + // Note how we're expecting each upstream request to hit the same upstream. + waitForNextUpstreamRequest(0); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + + if (fake_upstreams_[0]->httpType() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(fake_upstreams_[1]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + + waitForNextUpstreamRequest(1); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(1024U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + EXPECT_EQ(512U, response->body().size()); +} + +// +// Verifies that a retry host filter can be configured and affect the host selected during retries. +// The predicate will keep track of the first host attempted, and attempt to route all requests to +// the same host. With a total of two upstream hosts, this should result in us continuously sending +// requests to the same host. +TEST_P(DownstreamProtocolIntegrationTest, RetryHostPredicateFilter) { + TestHostPredicateFactory predicate_factory; + Registry::InjectFactory inject_factory(predicate_factory); + + envoy::api::v2::route::RetryPolicy retry_policy; + retry_policy.add_retry_host_predicate()->set_name(predicate_factory.name()); + + // Add route with custom retry policy + config_helper_.addRoute("host", "/test_retry", "cluster_0", false, + envoy::api::v2::route::RouteAction::NOT_FOUND, + envoy::api::v2::route::VirtualHost::NONE, retry_policy); + + // We want to work with a cluster with two hosts. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + auto* new_host = bootstrap.mutable_static_resources()->mutable_clusters(0)->add_hosts(); + new_host->MergeFrom(bootstrap.static_resources().clusters(0).hosts(0)); + }); + fake_upstreams_count_ = 2; + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test_retry"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024); + + // Note how we're expecting each upstream request to hit the same upstream. + auto upstream_idx = waitForNextUpstreamRequest({0, 1}); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + + if (fake_upstreams_[upstream_idx]->httpType() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + ASSERT_TRUE(fake_upstreams_[upstream_idx]->waitForHttpConnection(*dispatcher_, + fake_upstream_connection_)); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + } + + waitForNextUpstreamRequest(upstream_idx); + upstream_request_->encodeHeaders(default_response_headers_, false); + upstream_request_->encodeData(512, true); + + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(1024U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + EXPECT_EQ(512U, response->body().size()); +} + +// Very similar set-up to testRetry but with a 16k request the request will not +// be buffered and the 503 will be returned to the user. +TEST_P(ProtocolIntegrationTest, RetryHittingBufferLimit) { + config_helper_.setBufferLimits(1024, 1024); // Set buffer limits upstream and downstream. + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024 * 65); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, true); + + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_EQ(66560U, upstream_request_->bodyLength()); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); +} + +// Test hitting the dynamo filter with too many request bytes to buffer. Ensure the connection +// manager sends a 413. +TEST_P(DownstreamProtocolIntegrationTest, HittingDecoderFilterLimit) { + config_helper_.addFilter("{ name: envoy.http_dynamo_filter, config: {} }"); + config_helper_.setBufferLimits(1024, 1024); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Envoy will likely connect and proxy some unspecified amount of data before + // hitting the buffer limit and disconnecting. Ignore this if it happens. + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/dynamo/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + {"x-envoy-retry-on", "5xx"}}, + 1024 * 65); + + response->waitForEndStream(); + // With HTTP/1 there's a possible race where if the connection backs up early, + // the 413-and-connection-close may be sent while the body is still being + // sent, resulting in a write error and the connection being closed before the + // response is read. + if (downstream_protocol_ == Http::CodecClient::Type::HTTP2) { + ASSERT_TRUE(response->complete()); + } + if (response->complete()) { + EXPECT_STREQ("413", response->headers().Status()->value().c_str()); + } +} + +// Test hitting the dynamo filter with too many response bytes to buffer. Given the request headers +// are sent on early, the stream/connection will be reset. +TEST_P(DownstreamProtocolIntegrationTest, HittingEncoderFilterLimit) { + config_helper_.addFilter("{ name: envoy.http_dynamo_filter, config: {} }"); + config_helper_.setBufferLimits(1024, 1024); + initialize(); + + // Send the request. + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto downstream_request = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + Buffer::OwnedImpl data("{\"TableName\":\"locations\"}"); + codec_client_->sendData(*downstream_request, data, true); + waitForNextUpstreamRequest(); + + // Send the response headers. + upstream_request_->encodeHeaders(default_response_headers_, false); + + // Now send an overly large response body. At some point, too much data will + // be buffered, the stream will be reset, and the connection will disconnect. + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); + upstream_request_->encodeData(1024 * 65, false); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("500", response->headers().Status()->value().c_str()); +} + +TEST_P(ProtocolIntegrationTest, EnvoyHandling100Continue) { testEnvoyHandling100Continue(); } + +TEST_P(ProtocolIntegrationTest, EnvoyHandlingDuplicate100Continue) { + testEnvoyHandling100Continue(true); +} + +TEST_P(ProtocolIntegrationTest, EnvoyProxyingEarly100Continue) { + testEnvoyProxying100Continue(true); +} + +TEST_P(ProtocolIntegrationTest, EnvoyProxyingLate100Continue) { + testEnvoyProxying100Continue(false); +} + +TEST_P(ProtocolIntegrationTest, TwoRequests) { testTwoRequests(); } + +TEST_P(ProtocolIntegrationTest, TwoRequestsWithForcedBackup) { testTwoRequests(true); } + +TEST_P(DownstreamProtocolIntegrationTest, ValidZeroLengthContent) { + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + Http::TestHeaderMapImpl request_headers{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"content-length", "0"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + +TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLength) { + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto encoder_decoder = + codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":authority", "host"}, + {"content-length", "-1"}}); + auto response = std::move(encoder_decoder.second); + + if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { + codec_client_->waitForDisconnect(); + } else { + response->waitForReset(); + codec_client_->close(); + } + + if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("400", response->headers().Status()->value().c_str()); + } else { + ASSERT_TRUE(response->reset()); + EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->reset_reason()); + } +} + +TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengths) { + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = + codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":authority", "host"}, + {"content-length", "3,2"}}); + auto response = std::move(encoder_decoder.second); + + if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { + codec_client_->waitForDisconnect(); + } else { + response->waitForReset(); + codec_client_->close(); + } + + if (downstream_protocol_ == Http::CodecClient::Type::HTTP1) { + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("400", response->headers().Status()->value().c_str()); + } else { + ASSERT_TRUE(response->reset()); + EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->reset_reason()); + } +} + +TEST_P(DownstreamProtocolIntegrationTest, HeadersOnlyFilterEncoding) { + config_helper_.addFilter(R"EOF( +name: encode-headers-only +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}, + 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + response->waitForEndStream(); + EXPECT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); + if (upstreamProtocol() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + ASSERT_TRUE(fake_upstream_connection_->close()); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + } + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + EXPECT_EQ(0, response->body().size()); +} + +TEST_P(DownstreamProtocolIntegrationTest, HeadersOnlyFilterDecoding) { + config_helper_.addFilter(R"EOF( +name: decode-headers-only +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}, + 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + upstream_request_->encodeData(128, true); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + EXPECT_EQ(128, response->body().size()); +} + +TEST_P(DownstreamProtocolIntegrationTest, HeadersOnlyFilterEncodingIntermediateFilters) { + config_helper_.addFilter(R"EOF( +name: passthrough-filter +)EOF"); + config_helper_.addFilter(R"EOF( +name: encode-headers-only +)EOF"); + config_helper_.addFilter(R"EOF( +name: passthrough-filter +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}, + 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + response->waitForEndStream(); + if (upstreamProtocol() == FakeHttpConnection::Type::HTTP1) { + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + } else { + ASSERT_TRUE(upstream_request_->waitForReset()); + ASSERT_TRUE(fake_upstream_connection_->close()); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); + } + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + EXPECT_EQ(0, response->body().size()); +} + +TEST_P(DownstreamProtocolIntegrationTest, HeadersOnlyFilterDecodingIntermediateFilters) { + config_helper_.addFilter(R"EOF( +name: passthrough-filter +)EOF"); + config_helper_.addFilter(R"EOF( +name: decode-headers-only +)EOF"); + config_helper_.addFilter(R"EOF( +name: passthrough-filter +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = + codec_client_->makeRequestWithBody(Http::TestHeaderMapImpl{{":method", "POST"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}, + 128); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + upstream_request_->encodeData(128, true); + response->waitForEndStream(); + + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + EXPECT_EQ(128, response->body().size()); +} + +// Verifies behavior when request data is encoded after the request has been +// turned into a headers-only request and the response has already begun. +TEST_P(DownstreamProtocolIntegrationTest, HeadersOnlyFilterInterleaved) { + config_helper_.addFilter(R"EOF( +name: decode-headers-only +)EOF"); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + // First send the request headers. The filter should turn this into a header-only + // request. + auto encoder_decoder = + codec_client_->startRequest(Http::TestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}}); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + + // Wait for the upstream request and begin sending a response with end_stream = false. + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "503"}}, false); + + // Simulate additional data after the request has been turned into a headers only request. + Buffer::OwnedImpl data(std::string(128, 'a')); + request_encoder_->encodeData(data, false); + + // End the response. + upstream_request_->encodeData(128, true); + + response->waitForEndStream(); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("503", response->headers().Status()->value().c_str()); + EXPECT_EQ(0, upstream_request_->body().length()); +} + +// For tests which focus on downstream-to-Envoy behavior, and don't need to be +// run with both HTTP/1 and HTTP/2 upstreams. +INSTANTIATE_TEST_SUITE_P(Protocols, DownstreamProtocolIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams( + {Http::CodecClient::Type::HTTP1, Http::CodecClient::Type::HTTP2}, + {FakeHttpConnection::Type::HTTP1})), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +INSTANTIATE_TEST_SUITE_P(Protocols, ProtocolIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +} // namespace Envoy