diff --git a/api/envoy/extensions/filters/http/decompressor/v3/decompressor.proto b/api/envoy/extensions/filters/http/decompressor/v3/decompressor.proto index 82e1bc02d1bba..bfcd1c67bae9a 100644 --- a/api/envoy/extensions/filters/http/decompressor/v3/decompressor.proto +++ b/api/envoy/extensions/filters/http/decompressor/v3/decompressor.proto @@ -25,6 +25,9 @@ message Decompressor { // Runtime flag that controls whether the filter is enabled for decompression or not. If set to false, the // filter will operate as a pass-through filter. If the message is unspecified, the filter will be enabled. config.core.v3.RuntimeFeatureFlag enabled = 1; + + // If set to true, will decompress response even if a *no-transform* cache control header is set. + bool ignore_no_transform_header = 2; } // Configuration for filter behavior on the request direction. diff --git a/docs/root/configuration/http/http_filters/decompressor_filter.rst b/docs/root/configuration/http/http_filters/decompressor_filter.rst index 10919b5abb87e..b1be8b98d12f2 100644 --- a/docs/root/configuration/http/http_filters/decompressor_filter.rst +++ b/docs/root/configuration/http/http_filters/decompressor_filter.rst @@ -39,7 +39,9 @@ By *default* decompression will be *skipped* when: - A request/response does NOT contain *content-encoding* header. - A request/response includes *content-encoding* header, but it does not contain the configured decompressor's content-encoding. -- A request/response contains a *cache-control* header whose value includes "no-transform". +- A request/response contains a *cache-control* header whose value includes "no-transform", + unless :ref:`ignore_no_transform_header ` + is set to *true*. When decompression is *applied*: diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index fa9895f760eba..e36d3b8947aaa 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -70,6 +70,7 @@ New Features * bootstrap: added :ref:`typed_dns_resolver_config ` in the bootstrap to support DNS resolver as an extension. * cluster: added :ref:`typed_dns_resolver_config ` in the cluster to support DNS resolver as an extension. * config: added :ref:`environment_variable ` to the :ref:`DataSource `. +* decompressor: added :ref:`ignore_no_transform_header ` to run decompression regardless of the value of the *no-transform* cache control header. * dns: added :ref:`ALL ` option to return both IPv4 and IPv6 addresses. * dns_cache: added :ref:`typed_dns_resolver_config ` in the dns_cache to support DNS resolver as an extension. * dns_filter: added :ref:`typed_dns_resolver_config ` in the dns_filter to support DNS resolver as an extension. diff --git a/source/extensions/filters/http/decompressor/decompressor_filter.cc b/source/extensions/filters/http/decompressor/decompressor_filter.cc index d632c356004bd..03dc4d3e7f603 100644 --- a/source/extensions/filters/http/decompressor/decompressor_filter.cc +++ b/source/extensions/filters/http/decompressor/decompressor_filter.cc @@ -42,7 +42,8 @@ DecompressorFilterConfig::DirectionConfig::DirectionConfig( proto_config, const std::string& stats_prefix, Stats::Scope& scope, Runtime::Loader& runtime) : stats_(generateStats(stats_prefix, scope)), - decompression_enabled_(proto_config.enabled(), runtime) {} + decompression_enabled_(proto_config.enabled(), runtime), + ignore_no_transform_header_(proto_config.ignore_no_transform_header()) {} DecompressorFilterConfig::RequestDirectionConfig::RequestDirectionConfig( const envoy::extensions::filters::http::decompressor::v3::Decompressor::RequestDirectionConfig& diff --git a/source/extensions/filters/http/decompressor/decompressor_filter.h b/source/extensions/filters/http/decompressor/decompressor_filter.h index d1baf6f44035c..ebc704081978f 100644 --- a/source/extensions/filters/http/decompressor/decompressor_filter.h +++ b/source/extensions/filters/http/decompressor/decompressor_filter.h @@ -47,6 +47,7 @@ class DecompressorFilterConfig { virtual const std::string& logString() const PURE; const DecompressorStats& stats() const { return stats_; } bool decompressionEnabled() const { return decompression_enabled_.enabled(); } + bool ignoreNoTransformHeader() const { return ignore_no_transform_header_; } private: static DecompressorStats generateStats(const std::string& prefix, Stats::Scope& scope) { @@ -55,6 +56,7 @@ class DecompressorFilterConfig { const DecompressorStats stats_; const Runtime::FeatureFlag decompression_enabled_; + const bool ignore_no_transform_header_; }; class RequestDirectionConfig : public DirectionConfig { @@ -165,8 +167,11 @@ class DecompressorFilter : public Http::PassThroughFilter, maybeInitDecompress(const DecompressorFilterConfig::DirectionConfig& direction_config, Compression::Decompressor::DecompressorPtr& decompressor, Http::StreamFilterCallbacks& callbacks, HeaderType& headers) { - if (direction_config.decompressionEnabled() && !hasCacheControlNoTransform(headers) && - contentEncodingMatches(headers)) { + const bool should_decompress = + direction_config.decompressionEnabled() && + (!hasCacheControlNoTransform(headers) || direction_config.ignoreNoTransformHeader()) && + contentEncodingMatches(headers); + if (should_decompress) { direction_config.stats().decompressed_.inc(); decompressor = config_->makeDecompressor(); diff --git a/test/extensions/filters/http/decompressor/decompressor_filter_test.cc b/test/extensions/filters/http/decompressor/decompressor_filter_test.cc index f29fc45142b57..ded8ae26c78e1 100644 --- a/test/extensions/filters/http/decompressor/decompressor_filter_test.cc +++ b/test/extensions/filters/http/decompressor/decompressor_filter_test.cc @@ -391,6 +391,59 @@ TEST_P(DecompressorFilterTest, ResponseDecompressionDisabled) { } } +TEST_P(DecompressorFilterTest, DecompressionDisabledWhenNoTransformIsSet) { + EXPECT_CALL(*decompressor_factory_, createDecompressor(_)).Times(0); + Http::TestRequestHeaderMapImpl headers_before_filter{{"content-encoding", "mock, br , gzip "}, + {"content-length", "256"}, + {"cache-control", "no-transform"}}; + std::unique_ptr headers_after_filter = + doHeaders(headers_before_filter, false /* end_stream */); + + if (isRequestDirection()) { + ASSERT_EQ(headers_after_filter->get(Http::LowerCaseString("accept-encoding"))[0] + ->value() + .getStringView(), + "mock"); + // The request direction adds Accept-Encoding by default. Other than this header, the rest of + // the headers should be the same before and after the filter. + headers_after_filter->remove(Http::LowerCaseString("accept-encoding")); + } + EXPECT_THAT(headers_after_filter, HeaderMapEqualIgnoreOrder(&headers_before_filter)); + + expectNoDecompression(); +} + +TEST_P(DecompressorFilterTest, + DecompressionEnabledWhenNoTransformAndIgnoreNoTransformHeaderAreSet) { + setUpFilter(R"EOF( +decompressor_library: + typed_config: + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.decompressor.v3.Gzip" +response_direction_config: + common_config: + ignore_no_transform_header: true +)EOF"); + + Http::TestRequestHeaderMapImpl headers_before_filter{ + {"content-encoding", "mock"}, {"content-length", "256"}, {"cache-control", "no-transform"}}; + if (isRequestDirection()) { + EXPECT_CALL(*decompressor_factory_, createDecompressor(_)).Times(0); + std::unique_ptr headers_after_filter = + doHeaders(headers_before_filter, false /* end_stream */); + + // The request direction adds Accept-Encoding by default. Other than this header, the rest of + // the headers should be the same before and after the filter. + headers_after_filter->remove(Http::LowerCaseString("accept-encoding")); + EXPECT_THAT(headers_after_filter, HeaderMapEqualIgnoreOrder(&headers_before_filter)); + + expectNoDecompression(); + } else { + decompressionActive(headers_before_filter, true /* end_with_data */, + absl::nullopt /* expected_content_encoding*/, + "mock" /* expected_accept_encoding */); + } +} + TEST_P(DecompressorFilterTest, NoDecompressionHeadersOnly) { EXPECT_CALL(*decompressor_factory_, createDecompressor(_)).Times(0); Http::TestRequestHeaderMapImpl headers_before_filter;