Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ message ResponseDirectionOverrides {
message CompressorOverrides {
// If present, response compression is enabled.
ResponseDirectionOverrides response_direction_config = 1;

// A compressor library to use for compression. If specified, this overrides
// the filter-level ``compressor_library`` configuration for this route.
config.core.v3.TypedExtensionConfig compressor_library = 2;
}

message CompressorPerRoute {
Expand Down
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,13 @@ new_features:
Added support for generating and propagating a request ID on synthesized upstream HTTP requests when tunneling requests.
It can be configured using :ref:`request_id_extension
<envoy_v3_api_field_extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig.request_id_extension>`.
- area: http
change: |
Added support for per-route compressor library override in the HTTP compressor filter.
Routes can now specify a different compressor library (e.g., gzip, brotli) via the
:ref:`compressor_library <envoy_v3_api_field_extensions.filters.http.compressor.v3.CompressorOverrides.compressor_library>`
field in the per-route configuration. This allows different routes to use different
compression algorithms and settings while maintaining the same filter configuration.
- area: router_check_tool
change: |
Added support for testing routes with :ref:`dynamic metadata matchers <envoy_v3_api_field_config.route.v3.RouteMatch.dynamic_metadata>`
Expand Down
21 changes: 21 additions & 0 deletions docs/root/configuration/http/http_filters/compressor_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ For example, to disable response compression for a particular virtual host, but
:lines: 14-36
:caption: :download:`compressor-filter.yaml <_include/compressor-filter.yaml>`

Additionally, the compressor library can be overridden on a per-route basis. This allows
different routes to use different compression algorithms (e.g., gzip, brotli, zstd) while
maintaining the same filter configuration. For example, to use brotli compression for a
specific route while using gzip as the default:

.. code-block:: yaml

routes:
- match:
prefix: "/api"
route:
cluster: service
typed_per_filter_config:
envoy.filters.http.compressor:
"@type": type.googleapis.com/envoy.extensions.filters.http.compressor.v3.CompressorPerRoute
overrides:
compressor_library:
name: brotli
typed_config:
"@type": type.googleapis.com/envoy.extensions.compression.brotli.compressor.v3.Brotli

Using different compressors for requests and responses
--------------------------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions source/extensions/filters/http/compressor/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ envoy_cc_library(
srcs = ["compressor_filter.cc"],
hdrs = ["compressor_filter.h"],
deps = [
"//envoy/compression/compressor:compressor_config_interface",
"//envoy/compression/compressor:compressor_factory_interface",
"//envoy/registry",
"//envoy/stats:stats_macros",
"//source/common/config:utility_lib",
"//source/common/runtime:runtime_lib",
"//source/extensions/filters/http/common:pass_through_filter_lib",
"@envoy_api//envoy/extensions/filters/http/compressor/v3:pkg_cc_proto",
Expand All @@ -34,6 +37,7 @@ envoy_cc_extension(
"//envoy/compression/compressor:compressor_config_interface",
"//source/common/config:utility_lib",
"//source/extensions/filters/http/common:factory_base_lib",
"//source/server:generic_factory_context_lib",
"@envoy_api//envoy/extensions/filters/http/compressor/v3:pkg_cc_proto",
],
)
96 changes: 77 additions & 19 deletions source/extensions/filters/http/compressor/compressor_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

#include <cstdint>

#include "envoy/compression/compressor/config.h"
#include "envoy/registry/registry.h"

#include "source/common/buffer/buffer_impl.h"
#include "source/common/config/utility.h"
#include "source/common/http/header_map_impl.h"
#include "source/common/http/utility.h"
#include "source/common/protobuf/protobuf.h"
Expand Down Expand Up @@ -177,9 +181,18 @@ Envoy::Compression::Compressor::CompressorPtr CompressorFilterConfig::makeCompre

CompressorFilter::CompressorFilter(const CompressorFilterConfigSharedPtr config)
: config_(std::move(config)) {}
void CompressorFilter::initPerRouteConfig() {
if (decoder_callbacks_ == nullptr || per_route_config_ != nullptr) {
return;
}
per_route_config_ =
Http::Utility::resolveMostSpecificPerFilterConfig<CompressorPerRouteFilterConfig>(
decoder_callbacks_);
}

CompressorPerRouteFilterConfig::CompressorPerRouteFilterConfig(
const envoy::extensions::filters::http::compressor::v3::CompressorPerRoute& config) {
const envoy::extensions::filters::http::compressor::v3::CompressorPerRoute& config,
Server::Configuration::FactoryContext& context) {
switch (config.override_case()) {
case CompressorPerRoute::kDisabled:
response_compression_enabled_ = false;
Expand All @@ -196,6 +209,23 @@ CompressorPerRouteFilterConfig::CompressorPerRouteFilterConfig(
config.overrides().response_direction_config().remove_accept_encoding_header().value();
}
}

// Handle per-route compressor library configuration.
// Note: Validation of the compressor library type is done in config.cc before this
// constructor is called, so we can assume the factory exists.
if (config.overrides().has_compressor_library()) {
const std::string type{TypeUtil::typeUrlToDescriptorFullName(
config.overrides().compressor_library().typed_config().type_url())};
Compression::Compressor::NamedCompressorLibraryConfigFactory* const config_factory =
Registry::FactoryRegistry<
Compression::Compressor::NamedCompressorLibraryConfigFactory>::getFactoryByType(type);
ASSERT(config_factory != nullptr,
"Compressor library type should have been validated in config.cc");
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
config.overrides().compressor_library().typed_config(),
context.messageValidationVisitor(), *config_factory);
compressor_factory_ = config_factory->createCompressorFactoryFromProto(*message, context);
}
break;
case CompressorPerRoute::OVERRIDE_NOT_SET:
// This can't happen, because the `override` oneof has a `validate.required` PGV constraint,
Expand All @@ -213,12 +243,14 @@ Http::FilterHeadersStatus CompressorFilter::decodeHeaders(Http::RequestHeaderMap
accept_encoding_ = std::make_unique<std::string>(accept_encoding->value().getStringView());
}

// Ensure per-route configuration is initialized only once for this stream.
if (per_route_config_ == nullptr) {
initPerRouteConfig();
}

const auto& response_config = config_->responseDirectionConfig();
const auto* per_route_config =
Http::Utility::resolveMostSpecificPerFilterConfig<CompressorPerRouteFilterConfig>(
decoder_callbacks_);
if (compressionEnabled(response_config, per_route_config) &&
removeAcceptEncodingHeader(response_config, per_route_config)) {
if (compressionEnabled(response_config, per_route_config_) &&
removeAcceptEncodingHeader(response_config, per_route_config_)) {
headers.removeInline(accept_encoding_handle.handle());
}

Expand All @@ -230,9 +262,9 @@ Http::FilterHeadersStatus CompressorFilter::decodeHeaders(Http::RequestHeaderMap
!headers.getInline(request_content_encoding_handle.handle()) &&
isTransferEncodingAllowed(headers)) {
headers.removeContentLength();
headers.setInline(request_content_encoding_handle.handle(), config_->contentEncoding());
headers.setInline(request_content_encoding_handle.handle(), getContentEncoding());
request_config.stats().compressed_.inc();
request_compressor_ = config_->makeCompressor();
request_compressor_ = getCompressorFactory().createCompressor();
} else {
request_config.stats().not_compressed_.inc();
}
Expand Down Expand Up @@ -299,14 +331,15 @@ bool isResponseCodeCompressible(const Http::ResponseHeaderMap& headers,

Http::FilterHeadersStatus CompressorFilter::encodeHeaders(Http::ResponseHeaderMap& headers,
bool end_stream) {
// Ensure per-route config is initialized for encoder path as well.
if (per_route_config_ == nullptr) {
initPerRouteConfig();
}
const auto& config = config_->responseDirectionConfig();
const auto* per_route_config =
Http::Utility::resolveMostSpecificPerFilterConfig<CompressorPerRouteFilterConfig>(
decoder_callbacks_);

// This is used to decide whether stats for accept-encoding header should be touched.
const bool isEnabledAndContentLengthBigEnough =
compressionEnabled(config, per_route_config) && config.isMinimumContentLength(headers);
compressionEnabled(config, per_route_config_) && config.isMinimumContentLength(headers);

const bool isCompressible =
isEnabledAndContentLengthBigEnough && !Http::Utility::isUpgrade(headers) &&
Expand All @@ -317,10 +350,10 @@ Http::FilterHeadersStatus CompressorFilter::encodeHeaders(Http::ResponseHeaderMa
isCompressible && isTransferEncodingAllowed(headers)) {
sanitizeEtagHeader(headers);
headers.removeContentLength();
headers.setInline(response_content_encoding_handle.handle(), config_->contentEncoding());
headers.setInline(response_content_encoding_handle.handle(), getContentEncoding());
config.stats().compressed_.inc();
// Finally instantiate the compressor.
response_compressor_ = config_->makeCompressor();
response_compressor_ = getCompressorFactory().createCompressor();
} else {
config.stats().not_compressed_.inc();
}
Expand Down Expand Up @@ -417,10 +450,19 @@ CompressorFilter::chooseEncoding(const Http::ResponseHeaderMap& headers) const {
// case when there are two gzip filters using different compression levels for different content
// sizes. In such case we ignore duplicates (or different filters for the same encoding)
// registered last.
auto enc = allowed_compressors.find(filter_config->contentEncoding());
std::string content_encoding;
if (filter_config.get() == config_.get()) {
// For the current filter, use per-route content encoding if available.
content_encoding = getContentEncoding();
} else {
// For other filters in the chain, use their main config.
content_encoding = filter_config->contentEncoding();
}

auto enc = allowed_compressors.find(content_encoding);
if (enc == allowed_compressors.end()) {
allowed_compressors.insert(
{filter_config->contentEncoding(), {registration_count, filter_config->chooseFirst()}});
{content_encoding, {registration_count, filter_config->chooseFirst()}});
++registration_count;
}
}
Expand Down Expand Up @@ -511,8 +553,7 @@ CompressorFilter::chooseEncoding(const Http::ResponseHeaderMap& headers) const {
// Check if this filter was chosen to compress. Also update the filter's stat counters related to
// the Accept-Encoding header.
bool CompressorFilter::shouldCompress(const CompressorFilter::EncodingDecision& decision) const {
const bool should_compress =
absl::EqualsIgnoreCase(config_->contentEncoding(), decision.encoding());
const bool should_compress = absl::EqualsIgnoreCase(getContentEncoding(), decision.encoding());
const ResponseCompressorStats& stats = config_->responseDirectionConfig().responseStats();

switch (decision.stat()) {
Expand Down Expand Up @@ -635,7 +676,7 @@ bool CompressorFilter::isTransferEncodingAllowed(Http::RequestOrResponseHeaderMa
absl::EqualsIgnoreCase(trimmed_value, Http::Headers::get().TransferEncodingValues.Zstd) ||
// or with a custom non-standard compression provided by an external
// compression library.
absl::EqualsIgnoreCase(trimmed_value, config_->contentEncoding())) {
absl::EqualsIgnoreCase(trimmed_value, getContentEncoding())) {
return false;
}
}
Expand Down Expand Up @@ -692,6 +733,23 @@ bool CompressorFilter::removeAcceptEncodingHeader(
: config.removeAcceptEncodingHeader();
}

Envoy::Compression::Compressor::CompressorFactory& CompressorFilter::getCompressorFactory() const {
// Use cached per-route config if available.
if (per_route_config_ && per_route_config_->compressorFactory()) {
return const_cast<Envoy::Compression::Compressor::CompressorFactory&>(
*per_route_config_->compressorFactory());
}
return const_cast<Envoy::Compression::Compressor::CompressorFactory&>(
config_->compressorFactory());
}

std::string CompressorFilter::getContentEncoding() const {
if (per_route_config_ && per_route_config_->contentEncoding().has_value()) {
return per_route_config_->contentEncoding().value();
}
return config_->contentEncoding();
}

} // namespace Compressor
} // namespace HttpFilters
} // namespace Extensions
Expand Down
43 changes: 40 additions & 3 deletions source/extensions/filters/http/compressor/compressor_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

#include "envoy/compression/compressor/factory.h"
#include "envoy/extensions/filters/http/compressor/v3/compressor.pb.h"
#include "envoy/server/factory_context.h"
#include "envoy/stats/stats_macros.h"

#include "source/common/common/logger.h"
#include "source/common/protobuf/protobuf.h"
#include "source/common/runtime/runtime_protos.h"
#include "source/extensions/filters/http/common/pass_through_filter.h"
Expand Down Expand Up @@ -153,6 +155,9 @@ class CompressorFilterConfig {
bool chooseFirst() const { return choose_first_; };
const RequestDirectionConfig& requestDirectionConfig() { return request_direction_config_; }
const ResponseDirectionConfig& responseDirectionConfig() { return response_direction_config_; }
const Envoy::Compression::Compressor::CompressorFactory& compressorFactory() const {
return *compressor_factory_;
}

private:
const std::string common_stats_prefix_;
Expand All @@ -165,25 +170,40 @@ class CompressorFilterConfig {
};
using CompressorFilterConfigSharedPtr = std::shared_ptr<CompressorFilterConfig>;

class CompressorPerRouteFilterConfig : public Router::RouteSpecificFilterConfig {
class CompressorPerRouteFilterConfig : public Router::RouteSpecificFilterConfig,
public Logger::Loggable<Logger::Id::filter> {
public:
CompressorPerRouteFilterConfig(
const envoy::extensions::filters::http::compressor::v3::CompressorPerRoute& config);
const envoy::extensions::filters::http::compressor::v3::CompressorPerRoute& config,
Server::Configuration::FactoryContext& context);

// If a value is present, that value overrides
// ResponseDirectionConfig::compressionEnabled.
absl::optional<bool> responseCompressionEnabled() const { return response_compression_enabled_; }
absl::optional<bool> removeAcceptEncodingHeader() const { return remove_accept_encoding_header_; }

// Returns the per-route compressor factory if configured, nullptr otherwise.
const Envoy::Compression::Compressor::CompressorFactory* compressorFactory() const {
return compressor_factory_.get();
}

// Returns the content encoding for the per-route compressor if configured.
absl::optional<std::string> contentEncoding() const {
return compressor_factory_ ? absl::make_optional(compressor_factory_->contentEncoding())
: absl::nullopt;
}

private:
absl::optional<bool> response_compression_enabled_;
absl::optional<bool> remove_accept_encoding_header_;
Envoy::Compression::Compressor::CompressorFactoryPtr compressor_factory_;
};

/**
* A filter that compresses data dispatched from the upstream upon client request.
*/
class CompressorFilter : public Http::PassThroughFilter {
class CompressorFilter : public Http::PassThroughFilter,
public Logger::Loggable<Logger::Id::filter> {
public:
explicit CompressorFilter(const CompressorFilterConfigSharedPtr config);

Expand All @@ -200,7 +220,15 @@ class CompressorFilter : public Http::PassThroughFilter {
Http::FilterDataStatus encodeData(Buffer::Instance& buffer, bool end_stream) override;
Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap&) override;

// Grant testing peer access.
friend class CompressorFilterTestingPeer;

private:
// Initialize and cache the most specific per-route config only once for this stream.
// Subsequent accesses should use the cached pointer to avoid any inconsistencies if
// the route is refreshed mid-stream.
void initPerRouteConfig();

bool compressionEnabled(const CompressorFilterConfig::ResponseDirectionConfig& config,
const CompressorPerRouteFilterConfig* per_route_config) const;
bool removeAcceptEncodingHeader(const CompressorFilterConfig::ResponseDirectionConfig& config,
Expand Down Expand Up @@ -234,10 +262,19 @@ class CompressorFilter : public Http::PassThroughFilter {
std::unique_ptr<EncodingDecision> chooseEncoding(const Http::ResponseHeaderMap& headers) const;
bool shouldCompress(const EncodingDecision& decision) const;

// Returns the appropriate compressor factory for the current route.
// Checks for per-route config first, then falls back to main config.
Envoy::Compression::Compressor::CompressorFactory& getCompressorFactory() const;

// Returns the appropriate content encoding for the current route.
std::string getContentEncoding() const;

Envoy::Compression::Compressor::CompressorPtr response_compressor_;
Envoy::Compression::Compressor::CompressorPtr request_compressor_;
const CompressorFilterConfigSharedPtr config_;
std::unique_ptr<std::string> accept_encoding_;
// Cached per-route configuration pointer, initialized once per stream.
const CompressorPerRouteFilterConfig* per_route_config_{};
};

} // namespace Compressor
Expand Down
Loading