diff --git a/api/envoy/extensions/filters/http/compressor/v3/compressor.proto b/api/envoy/extensions/filters/http/compressor/v3/compressor.proto index 0c8d28602dade..584195501aeee 100644 --- a/api/envoy/extensions/filters/http/compressor/v3/compressor.proto +++ b/api/envoy/extensions/filters/http/compressor/v3/compressor.proto @@ -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 { diff --git a/changelogs/current.yaml b/changelogs/current.yaml index fc9e053e6c480..ef166334c6646 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -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 `. +- 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 ` + 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 ` diff --git a/docs/root/configuration/http/http_filters/compressor_filter.rst b/docs/root/configuration/http/http_filters/compressor_filter.rst index 9625318436c5b..b3b18465ceadb 100644 --- a/docs/root/configuration/http/http_filters/compressor_filter.rst +++ b/docs/root/configuration/http/http_filters/compressor_filter.rst @@ -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 -------------------------------------------------------- diff --git a/source/extensions/filters/http/compressor/BUILD b/source/extensions/filters/http/compressor/BUILD index 1dd1e309e42cf..218d64538f960 100644 --- a/source/extensions/filters/http/compressor/BUILD +++ b/source/extensions/filters/http/compressor/BUILD @@ -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", @@ -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", ], ) diff --git a/source/extensions/filters/http/compressor/compressor_filter.cc b/source/extensions/filters/http/compressor/compressor_filter.cc index 5086d29f3f3be..b5bd709d5ea7d 100644 --- a/source/extensions/filters/http/compressor/compressor_filter.cc +++ b/source/extensions/filters/http/compressor/compressor_filter.cc @@ -2,7 +2,11 @@ #include +#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" @@ -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( + 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; @@ -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, @@ -213,12 +243,14 @@ Http::FilterHeadersStatus CompressorFilter::decodeHeaders(Http::RequestHeaderMap accept_encoding_ = std::make_unique(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( - 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()); } @@ -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(); } @@ -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( - 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) && @@ -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(); } @@ -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; } } @@ -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()) { @@ -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; } } @@ -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( + *per_route_config_->compressorFactory()); + } + return const_cast( + 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 diff --git a/source/extensions/filters/http/compressor/compressor_filter.h b/source/extensions/filters/http/compressor/compressor_filter.h index c98117d7122f8..ac1420fcc589f 100644 --- a/source/extensions/filters/http/compressor/compressor_filter.h +++ b/source/extensions/filters/http/compressor/compressor_filter.h @@ -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" @@ -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_; @@ -165,25 +170,40 @@ class CompressorFilterConfig { }; using CompressorFilterConfigSharedPtr = std::shared_ptr; -class CompressorPerRouteFilterConfig : public Router::RouteSpecificFilterConfig { +class CompressorPerRouteFilterConfig : public Router::RouteSpecificFilterConfig, + public Logger::Loggable { 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 responseCompressionEnabled() const { return response_compression_enabled_; } absl::optional 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 contentEncoding() const { + return compressor_factory_ ? absl::make_optional(compressor_factory_->contentEncoding()) + : absl::nullopt; + } + private: absl::optional response_compression_enabled_; absl::optional 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 { public: explicit CompressorFilter(const CompressorFilterConfigSharedPtr config); @@ -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, @@ -234,10 +262,19 @@ class CompressorFilter : public Http::PassThroughFilter { std::unique_ptr 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 accept_encoding_; + // Cached per-route configuration pointer, initialized once per stream. + const CompressorPerRouteFilterConfig* per_route_config_{}; }; } // namespace Compressor diff --git a/source/extensions/filters/http/compressor/config.cc b/source/extensions/filters/http/compressor/config.cc index 8ca814d787217..7f7e4d02d7038 100644 --- a/source/extensions/filters/http/compressor/config.cc +++ b/source/extensions/filters/http/compressor/config.cc @@ -1,9 +1,14 @@ #include "source/extensions/filters/http/compressor/config.h" #include "envoy/compression/compressor/config.h" +#include "envoy/config/typed_metadata.h" +#include "envoy/network/address.h" #include "source/common/config/utility.h" +#include "source/common/network/address_impl.h" +#include "source/common/network/utility.h" #include "source/extensions/filters/http/compressor/compressor_filter.h" +#include "source/server/generic_factory_context.h" namespace Envoy { namespace Extensions { @@ -35,11 +40,100 @@ absl::StatusOr CompressorFilterFactory::createFilterFacto }; } +namespace { + +// Simple null implementations for the route factory context wrapper +struct NullDrainDecision : public Network::DrainDecision { + bool drainClose(Network::DrainDirection) const override { return false; } + ::Envoy::Common::CallbackHandlePtr addOnDrainCloseCb(Network::DrainDirection, + DrainCloseCb) const override { + return nullptr; + } +}; + +struct NullListenerInfo : public Network::ListenerInfo { + const envoy::config::core::v3::Metadata& metadata() const override { + static const envoy::config::core::v3::Metadata metadata; + return metadata; + } + const Envoy::Config::TypedMetadata& typedMetadata() const override { + // Simple empty typed metadata implementation for the null listener info + class EmptyTypedMetadata : public Envoy::Config::TypedMetadata { + public: + const Object* getData(const std::string&) const override { return nullptr; } + }; + static const EmptyTypedMetadata metadata; + return metadata; + } + envoy::config::core::v3::TrafficDirection direction() const override { + return envoy::config::core::v3::TrafficDirection::UNSPECIFIED; + } + bool isQuic() const override { return false; } + bool shouldBypassOverloadManager() const override { return false; } + const Network::Address::Instance& address() const { + static auto address = Network::Address::InstanceConstSharedPtr{ + new Network::Address::Ipv4Instance("0.0.0.0", static_cast(0))}; + return *address; + } + absl::string_view name() const { return "null_listener"; } + Network::UdpListenerConfigOptRef udpListenerConfig() { return {}; } +}; + +} // namespace + absl::StatusOr CompressorFilterFactory::createRouteSpecificFilterConfigTyped( const envoy::extensions::filters::http::compressor::v3::CompressorPerRoute& proto_config, - Server::Configuration::ServerFactoryContext&, ProtobufMessage::ValidationVisitor&) { - return std::make_shared(proto_config); + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) { + // Validate per-route compressor library configuration before creating the config object. + if (proto_config.has_overrides() && proto_config.overrides().has_compressor_library()) { + const std::string type{TypeUtil::typeUrlToDescriptorFullName( + proto_config.overrides().compressor_library().typed_config().type_url())}; + Compression::Compressor::NamedCompressorLibraryConfigFactory* const config_factory = + Registry::FactoryRegistry< + Compression::Compressor::NamedCompressorLibraryConfigFactory>::getFactoryByType(type); + if (config_factory == nullptr) { + return absl::InvalidArgumentError(fmt::format( + "Didn't find a registered implementation for per-route compressor type: '{}'", type)); + } + } + + // Create a temporary factory context that wraps the generic factory context. + // Other filters commonly use GenericFactoryContextImpl for per-route config construction. + // Since the compressor library factory requires a FactoryContext, adapt GenericFactoryContextImpl + // by providing minimal implementations for listener-specific methods. + Server::GenericFactoryContextImpl generic_context(context, validator); + struct RouteFactoryContextWrapper : public Server::Configuration::FactoryContext { + explicit RouteFactoryContextWrapper(Server::GenericFactoryContextImpl& generic) + : generic_(generic) {} + + // GenericFactoryContext methods + Server::Configuration::ServerFactoryContext& serverFactoryContext() override { + return generic_.serverFactoryContext(); + } + ProtobufMessage::ValidationVisitor& messageValidationVisitor() override { + return generic_.messageValidationVisitor(); + } + Init::Manager& initManager() override { return generic_.initManager(); } + Stats::Scope& scope() override { return generic_.scope(); } + + // FactoryContext methods + Stats::Scope& listenerScope() override { return generic_.scope(); } + const Network::DrainDecision& drainDecision() override { + static NullDrainDecision null_drain; + return null_drain; + } + const Network::ListenerInfo& listenerInfo() const override { + static NullListenerInfo null_listener_info; + return null_listener_info; + } + + private: + Server::GenericFactoryContextImpl& generic_; + } wrapper(generic_context); + + return std::make_shared(proto_config, wrapper); } /** diff --git a/test/extensions/filters/http/compressor/BUILD b/test/extensions/filters/http/compressor/BUILD index e8d446c55e3bf..8182d09aec131 100644 --- a/test/extensions/filters/http/compressor/BUILD +++ b/test/extensions/filters/http/compressor/BUILD @@ -20,6 +20,7 @@ envoy_extension_cc_test( name = "compressor_filter_test", srcs = [ "compressor_filter_test.cc", + "compressor_filter_testing_peer.h", ], extension_names = ["envoy.filters.http.compressor"], rbe_pool = "6gig", @@ -28,7 +29,9 @@ envoy_extension_cc_test( "//source/extensions/filters/http/compressor:compressor_filter_lib", "//test/mocks/compression/compressor:compressor_mocks", "//test/mocks/http:http_mocks", + "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/runtime:runtime_mocks", + "//test/mocks/server:factory_context_mocks", "//test/test_common:utility_lib", ], ) @@ -42,12 +45,16 @@ envoy_extension_cc_test( extension_names = ["envoy.filters.http.compressor"], rbe_pool = "6gig", deps = [ + "//source/extensions/compression/brotli/compressor:config", + "//source/extensions/compression/brotli/decompressor:config", "//source/extensions/compression/gzip/compressor:config", "//source/extensions/compression/gzip/decompressor:config", "//source/extensions/filters/http/compressor:config", "//test/integration:http_integration_lib", "//test/test_common:simulated_time_system_lib", "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/compression/brotli/compressor/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/compression/gzip/compressor/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/compressor/v3:pkg_cc_proto", ], ) @@ -66,9 +73,11 @@ envoy_extension_cc_test( rbe_pool = "6gig", deps = [ ":mock_config_cc_proto", + "//source/extensions/compression/common/compressor:compressor_factory_base_lib", "//source/extensions/filters/http/compressor:config", "//test/mocks/runtime:runtime_mocks", "//test/mocks/server:factory_context_mocks", + "//test/test_common:registry_lib", "//test/test_common:utility_lib", ], ) diff --git a/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc b/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc index 555b13e28d1d2..97b46ee018eb1 100644 --- a/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc +++ b/test/extensions/filters/http/compressor/compressor_filter_integration_test.cc @@ -1,7 +1,10 @@ #include "envoy/event/timer.h" +#include "envoy/extensions/compression/brotli/compressor/v3/brotli.pb.h" +#include "envoy/extensions/compression/gzip/compressor/v3/gzip.pb.h" #include "envoy/extensions/filters/http/compressor/v3/compressor.pb.h" #include "source/common/protobuf/protobuf.h" +#include "source/extensions/compression/brotli/decompressor/brotli_decompressor_impl.h" #include "source/extensions/compression/gzip/decompressor/zlib_decompressor_impl.h" #include "test/integration/http_integration.h" @@ -416,17 +419,19 @@ TEST_P(CompressorIntegrationTest, CompressedRequestAcceptanceFullConfigTest) { // Enable filter, then disable per-route. TEST_P(CompressorIntegrationTest, PerRouteDisable) { - config_helper_.addConfigModifier([](ConfigHelper::HttpConnectionManager& cm) { - auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); - auto* route = vh->mutable_routes()->Mutable(0); - route->mutable_match()->set_path("/nocompress"); - envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; - per_route.set_disabled(true); - Any cfg_any; - ASSERT_TRUE(cfg_any.PackFrom(per_route)); - route->mutable_typed_per_filter_config()->insert( - MapPair("envoy.filters.http.compressor", cfg_any)); - }); + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cm) { + auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); + auto* route = vh->mutable_routes()->Mutable(0); + route->mutable_match()->set_path("/nocompress"); + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + per_route.set_disabled(true); + Any cfg_any; + ASSERT_TRUE(cfg_any.PackFrom(per_route)); + route->mutable_typed_per_filter_config()->insert( + MapPair("envoy.filters.http.compressor", cfg_any)); + }); initializeFilter(R"EOF( name: envoy.filters.http.compressor typed_config: @@ -456,17 +461,19 @@ TEST_P(CompressorIntegrationTest, PerRouteDisable) { // Disable filter, then enable per-route. TEST_P(CompressorIntegrationTest, PerRouteEnable) { - config_helper_.addConfigModifier([](ConfigHelper::HttpConnectionManager& cm) { - auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); - auto* route = vh->mutable_routes()->Mutable(0); - route->mutable_match()->set_path("/compress"); - envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; - per_route.mutable_overrides()->mutable_response_direction_config(); - Any cfg_any; - ASSERT_TRUE(cfg_any.PackFrom(per_route)); - route->mutable_typed_per_filter_config()->insert( - MapPair("envoy.filters.http.compressor", cfg_any)); - }); + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cm) { + auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); + auto* route = vh->mutable_routes()->Mutable(0); + route->mutable_match()->set_path("/compress"); + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + per_route.mutable_overrides()->mutable_response_direction_config(); + Any cfg_any; + ASSERT_TRUE(cfg_any.PackFrom(per_route)); + route->mutable_typed_per_filter_config()->insert( + MapPair("envoy.filters.http.compressor", cfg_any)); + }); initializeFilter(R"EOF( name: envoy.filters.http.compressor typed_config: @@ -493,4 +500,118 @@ TEST_P(CompressorIntegrationTest, PerRouteEnable) { {"content-type", "text/xml"}}); } +// Test per-route compressor library override with brotli. +TEST_P(CompressorIntegrationTest, PerRouteCompressorLibraryOverride) { + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cm) { + auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); + auto* route = vh->mutable_routes()->Mutable(0); + route->mutable_match()->set_path("/brotli-per-route"); + + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + // Override the compressor library to use brotli instead of gzip. + auto* compressor_lib = per_route.mutable_overrides()->mutable_compressor_library(); + compressor_lib->set_name("brotli"); + compressor_lib->mutable_typed_config()->set_type_url( + "type.googleapis.com/envoy.extensions.compression.brotli.compressor.v3.Brotli"); + compressor_lib->mutable_typed_config()->set_value("{}"); + + Any cfg_any; + ASSERT_TRUE(cfg_any.PackFrom(per_route)); + route->mutable_typed_per_filter_config()->insert( + MapPair("envoy.filters.http.compressor", cfg_any)); + }); + + initializeFilter(R"EOF( + name: envoy.filters.http.compressor + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + compressor_library: + name: gzip-default + typed_config: + "@type": type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip + response_direction_config: + common_config: + enabled: + default_value: true + content_type: + - text/html + - application/json + )EOF"); + + // Request to /brotli-per-route should use brotli compression (per-route override). + auto response = sendRequestAndWaitForResponse( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/brotli-per-route"}, + {":scheme", "http"}, + {":authority", "host"}, + {"accept-encoding", "br, gzip"}}, + 0, + Http::TestResponseHeaderMapImpl{ + {":status", "200"}, {"content-length", "40"}, {"content-type", "text/html"}}, + 40); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + // Should be compressed with brotli (not gzip), validating the per-route override works. + Http::HeaderMap::GetResult content_encoding = + response->headers().get(Http::CustomHeaders::get().ContentEncoding); + ASSERT_FALSE(content_encoding.empty()); + EXPECT_EQ("br", content_encoding[0]->value().getStringView()); +} + +// Test that per-route compressor library config creation works with various libraries. +TEST_P(CompressorIntegrationTest, PerRouteCompressorLibraryConfigCreation) { + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cm) { + auto* vh = cm.mutable_route_config()->mutable_virtual_hosts()->Mutable(0); + auto* route = vh->mutable_routes()->Mutable(0); + route->mutable_match()->set_path("/custom"); + + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + // Test that per-route config can be created with different compressor library. + auto* compressor_lib = per_route.mutable_overrides()->mutable_compressor_library(); + compressor_lib->set_name("custom"); + compressor_lib->mutable_typed_config()->set_type_url( + "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip"); + + // Set some config for gzip compressor. + envoy::extensions::compression::gzip::compressor::v3::Gzip gzip_config; + gzip_config.mutable_window_bits()->set_value(12); + std::string serialized_config; + ASSERT_TRUE(gzip_config.SerializeToString(&serialized_config)); + compressor_lib->mutable_typed_config()->set_value(serialized_config); + + Any cfg_any; + ASSERT_TRUE(cfg_any.PackFrom(per_route)); + route->mutable_typed_per_filter_config()->insert( + MapPair("envoy.filters.http.compressor", cfg_any)); + }); + + initializeFilter(default_config); + + // Make request and verify it works. + // Both should compress with gzip but different settings. + auto response = sendRequestAndWaitForResponse( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/custom"}, + {":scheme", "http"}, + {":authority", "host"}, + {"accept-encoding", "gzip"}}, + 0, + Http::TestResponseHeaderMapImpl{ + {":status", "200"}, {"content-length", "40"}, {"content-type", "text/html"}}, + 40); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + Http::HeaderMap::GetResult content_encoding = + response->headers().get(Http::CustomHeaders::get().ContentEncoding); + ASSERT_FALSE(content_encoding.empty()); + EXPECT_EQ(Http::CustomHeaders::get().ContentEncodingValues.Gzip, + content_encoding[0]->value().getStringView()); +} + } // namespace Envoy diff --git a/test/extensions/filters/http/compressor/compressor_filter_test.cc b/test/extensions/filters/http/compressor/compressor_filter_test.cc index d3ab95afd078f..98d3e96b6e7b0 100644 --- a/test/extensions/filters/http/compressor/compressor_filter_test.cc +++ b/test/extensions/filters/http/compressor/compressor_filter_test.cc @@ -2,9 +2,12 @@ #include "source/extensions/filters/http/compressor/compressor_filter.h" +#include "test/extensions/filters/http/compressor/compressor_filter_testing_peer.h" #include "test/mocks/compression/compressor/mocks.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/protobuf/mocks.h" #include "test/mocks/runtime/mocks.h" +#include "test/mocks/server/factory_context.h" #include "test/mocks/stats/mocks.h" #include "test/mocks/stream_info/mocks.h" #include "test/test_common/utility.h" @@ -186,6 +189,8 @@ class CompressorFilterTest : public testing::Test { NiceMock decoder_callbacks_; NiceMock encoder_callbacks_; NiceMock stream_info_; + NiceMock factory_context_; + NiceMock server_factory_context_; }; enum class PerRouteConfig { None, Empty, Enabled, Disabled }; @@ -257,7 +262,8 @@ TEST_P(CompresorFilterEnablementTest, DecodeHeadersWithRuntimeDisabled) { } std::unique_ptr per_route_config; if (use_per_route_proto) { - per_route_config = std::make_unique(per_route_proto); + per_route_config = + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); } @@ -488,7 +494,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { per_route_proto.mutable_overrides()->mutable_response_direction_config(); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -512,7 +518,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { per_route_proto.mutable_overrides()->mutable_response_direction_config(); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -541,7 +547,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { ->set_value(true); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -569,7 +575,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { ->set_value(false); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -597,7 +603,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { ->set_value(true); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -624,7 +630,7 @@ TEST_F(CompressorFilterTest, RemoveAcceptEncodingHeader) { ->set_value(false); std::unique_ptr per_route_config = - std::make_unique(per_route_proto); + std::make_unique(per_route_proto, factory_context_); ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) .WillByDefault(Return(per_route_config.get())); @@ -1242,6 +1248,149 @@ TEST(CompressorFilterConfigTests, MakeCompressorTest) { Envoy::Compression::Compressor::CompressorPtr compressor = config.makeCompressor(); } +// Tests for per-route compressor library override functionality. +class CompressorPerRouteLibraryTest : public CompressorFilterTest { +public: + CompressorPerRouteLibraryTest() { + // Create a second compressor factory for testing different libraries. + deflate_compressor_factory_ = std::make_unique("deflate"); + } + +protected: + void setUpPerRouteConfig(const std::string& compressor_name, + const std::string& compressor_type_url) { + CompressorPerRoute per_route_proto; + auto* compressor_lib = per_route_proto.mutable_overrides()->mutable_compressor_library(); + compressor_lib->set_name(compressor_name); + compressor_lib->mutable_typed_config()->set_type_url(compressor_type_url); + compressor_lib->mutable_typed_config()->set_value("{}"); // Empty config for testing. + + // Mock the factory registry to return our test factory. + ON_CALL(factory_context_, messageValidationVisitor()) + .WillByDefault(ReturnRef(validation_visitor_)); + ON_CALL(factory_context_, serverFactoryContext()) + .WillByDefault(ReturnRef(server_factory_context_)); + + per_route_config_ = + std::make_unique(per_route_proto, factory_context_); + ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(per_route_config_.get())); + } + + std::unique_ptr deflate_compressor_factory_; + std::unique_ptr per_route_config_; + NiceMock validation_visitor_; +}; + +TEST_F(CompressorPerRouteLibraryTest, PerRouteCompressorFactoryIsUsed) { + // Set up main filter with "test" compressor. + setUpFilter(R"EOF( +{ + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + + // Test a valid per-route config creation without compressor library override. + CompressorPerRoute per_route_proto; + per_route_proto.mutable_overrides()->mutable_response_direction_config(); + + // This test verifies the per-route config structure is set up correctly. + per_route_config_ = + std::make_unique(per_route_proto, factory_context_); + + // Verify that per-route config without compressor library has no override. + EXPECT_EQ(per_route_config_->compressorFactory(), nullptr); + EXPECT_FALSE(per_route_config_->contentEncoding().has_value()); +} + +TEST_F(CompressorPerRouteLibraryTest, NoPerRouteCompressorLibrary) { + // Set up main filter. + setUpFilter(R"EOF( +{ + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + + // Per-route config without compressor library override. + CompressorPerRoute per_route_proto; + per_route_proto.mutable_overrides()->mutable_response_direction_config(); + + per_route_config_ = + std::make_unique(per_route_proto, factory_context_); + + // Verify that per-route config has no compressor factory override. + EXPECT_EQ(per_route_config_->compressorFactory(), nullptr); + EXPECT_FALSE(per_route_config_->contentEncoding().has_value()); +} + +TEST_F(CompressorPerRouteLibraryTest, GetContentEncodingUsesPerRoute) { + setUpFilter(R"EOF( +{ + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + + // Test without per-route config. It should use main config. + EXPECT_EQ(CompressorFilterTestingPeer::contentEncoding(*filter_), "test"); + + // Set up per-route config without compressor library. It should still use main config. + CompressorPerRoute per_route_proto; + per_route_proto.mutable_overrides()->mutable_response_direction_config(); + + per_route_config_ = + std::make_unique(per_route_proto, factory_context_); + ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(per_route_config_.get())); + + // Should still use main config content encoding. + EXPECT_EQ(CompressorFilterTestingPeer::contentEncoding(*filter_), "test"); +} + +TEST_F(CompressorPerRouteLibraryTest, GetCompressorFactoryUsesPerRoute) { + setUpFilter(R"EOF( +{ + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + + // Test without per-route config. It should use main config factory. + const auto& main_factory = CompressorFilterTestingPeer::compressorFactory(*filter_); + EXPECT_EQ(main_factory.contentEncoding(), "test"); + + // Set up per-route config without compressor library. It should still use main factory. + CompressorPerRoute per_route_proto; + per_route_proto.mutable_overrides()->mutable_response_direction_config(); + + per_route_config_ = + std::make_unique(per_route_proto, factory_context_); + ON_CALL(decoder_callbacks_, mostSpecificPerFilterConfig()) + .WillByDefault(Return(per_route_config_.get())); + + // Should still use main factory since no per-route compressor library override. + const auto& per_route_factory = CompressorFilterTestingPeer::compressorFactory(*filter_); + EXPECT_EQ(per_route_factory.contentEncoding(), "test"); +} + } // namespace } // namespace Compressor } // namespace HttpFilters diff --git a/test/extensions/filters/http/compressor/compressor_filter_testing_peer.h b/test/extensions/filters/http/compressor/compressor_filter_testing_peer.h new file mode 100644 index 0000000000000..00bb0146caa63 --- /dev/null +++ b/test/extensions/filters/http/compressor/compressor_filter_testing_peer.h @@ -0,0 +1,25 @@ +#pragma once + +#include "source/extensions/filters/http/compressor/compressor_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace Compressor { + +class CompressorFilterTestingPeer { +public: + static std::string contentEncoding(const CompressorFilter& filter) { + return filter.getContentEncoding(); + } + + static Envoy::Compression::Compressor::CompressorFactory& + compressorFactory(const CompressorFilter& filter) { + return filter.getCompressorFactory(); + } +}; + +} // namespace Compressor +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/compressor/config_test.cc b/test/extensions/filters/http/compressor/config_test.cc index da7b1dff362a0..82f9259ba766f 100644 --- a/test/extensions/filters/http/compressor/config_test.cc +++ b/test/extensions/filters/http/compressor/config_test.cc @@ -1,7 +1,14 @@ +#include "envoy/compression/compressor/config.h" +#include "envoy/compression/compressor/factory.h" +#include "envoy/network/drain_decision.h" +#include "envoy/network/listener.h" + #include "source/extensions/filters/http/compressor/config.h" #include "test/extensions/filters/http/compressor/mock_compressor_library.pb.h" #include "test/mocks/server/factory_context.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -33,6 +40,123 @@ TEST(CompressorFilterFactoryTests, UnregisteredCompressorLibraryConfig) { "'test.mock_compressor_library.Unregistered'")); } +// Minimal no-op compressor factory to inject and validate registered path. +class TestNoopCompressorFactory : public Envoy::Compression::Compressor::CompressorFactory { +public: + Envoy::Compression::Compressor::CompressorPtr createCompressor() override { + return nullptr; // not used + } + const std::string& statsPrefix() const override { + static const std::string p{"test_noop."}; + return p; + } + const std::string& contentEncoding() const override { + static const std::string e{"noop"}; + return e; + } +}; + +class TestNoopCompressorLibraryFactory + : public Envoy::Compression::Compressor::NamedCompressorLibraryConfigFactory { +public: + TestNoopCompressorLibraryFactory() = default; + +private: + Envoy::Compression::Compressor::CompressorFactoryPtr + createCompressorFactoryFromProto(const Protobuf::Message& /*config*/, + Server::Configuration::FactoryContext& /*context*/) override { + return std::make_unique(); + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique<::test::mock_compressor_library::Registered>(); + } + + std::string name() const override { return "test.mock.noop"; } + std::string category() const override { return "envoy.compression.compressor"; } +}; + +TEST(CompressorFilterFactoryTests, RegisteredCompressorLibraryConfig) { + const std::string yaml_string = R"EOF( + compressor_library: + name: test.mock.noop + typed_config: + "@type": type.googleapis.com/test.mock_compressor_library.Registered + )EOF"; + + envoy::extensions::filters::http::compressor::v3::Compressor proto_config; + TestUtility::loadFromYaml(yaml_string, proto_config); + CompressorFilterFactory factory; + NiceMock context; + + TestNoopCompressorLibraryFactory factory_impl; + Envoy::Registry::InjectFactory< + Envoy::Compression::Compressor::NamedCompressorLibraryConfigFactory> + reg(factory_impl); + auto cb_or = factory.createFilterFactoryFromProto(proto_config, "stats", context); + EXPECT_TRUE(cb_or.status().ok()); +} + +// Factory that touches drainDecision() and listenerInfo() on FactoryContext to cover wrapper. +class TestCheckingCompressorLibraryFactory + : public Envoy::Compression::Compressor::NamedCompressorLibraryConfigFactory { +public: + TestCheckingCompressorLibraryFactory() = default; + + Envoy::Compression::Compressor::CompressorFactoryPtr + createCompressorFactoryFromProto(const Protobuf::Message& /*config*/, + Server::Configuration::FactoryContext& context) override { + (void)context.serverFactoryContext(); + (void)context.messageValidationVisitor(); + (void)context.initManager(); + (void)context.scope(); + (void)context.listenerScope(); + (void)context.drainDecision().drainClose(Network::DrainDirection::All); + (void)context.drainDecision().addOnDrainCloseCb( + Network::DrainDirection::All, [](std::chrono::milliseconds) { return absl::OkStatus(); }); + const auto& info = context.listenerInfo(); + (void)info.metadata(); + (void)info.typedMetadata(); + (void)info.direction(); + (void)info.isQuic(); + (void)info.shouldBypassOverloadManager(); + + return std::make_unique(); + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique<::test::mock_compressor_library::Registered>(); + } + + std::string name() const override { return "test.mock.check"; } + std::string category() const override { return "envoy.compression.compressor"; } +}; + +TEST(CompressorFilterFactoryTests, PerRouteWrapperCoversDrainAndListenerInfo) { + // Per-route config with a typed compressor_library using the checking factory. + const std::string yaml_string = R"EOF( + overrides: + response_direction_config: {} + compressor_library: + name: test.mock.check + typed_config: + "@type": type.googleapis.com/test.mock_compressor_library.Registered + )EOF"; + + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + TestUtility::loadFromYaml(yaml_string, per_route); + NiceMock context; + CompressorFilterFactory factory; + TestCheckingCompressorLibraryFactory checking_impl; + Envoy::Registry::InjectFactory< + Envoy::Compression::Compressor::NamedCompressorLibraryConfigFactory> + reg(checking_impl); + + auto cfg_or = factory.createRouteSpecificFilterConfig(per_route, context, + context.messageValidationVisitor()); + EXPECT_TRUE(cfg_or.status().ok()); +} + TEST(CompressorFilterFactoryTests, EmptyPerRouteConfig) { envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; NiceMock context; @@ -44,6 +168,18 @@ TEST(CompressorFilterFactoryTests, EmptyPerRouteConfig) { ProtoValidationException); } +TEST(CompressorFilterFactoryTests, PerRouteWrapperBuilds) { + // Provide a minimally valid per-route proto: set overrides with empty response_direction_config + envoy::extensions::filters::http::compressor::v3::CompressorPerRoute per_route; + per_route.mutable_overrides()->mutable_response_direction_config(); + NiceMock context; + CompressorFilterFactory factory; + auto cfg_or = factory.createRouteSpecificFilterConfig(per_route, context, + context.messageValidationVisitor()); + EXPECT_TRUE(cfg_or.status().ok()); + // No further assertions; this exercises the GenericFactoryContext wrapper path. +} + } // namespace } // namespace Compressor } // namespace HttpFilters diff --git a/test/extensions/filters/http/compressor/mock_compressor_library.proto b/test/extensions/filters/http/compressor/mock_compressor_library.proto index 789f50aa33fea..521ce991fd474 100644 --- a/test/extensions/filters/http/compressor/mock_compressor_library.proto +++ b/test/extensions/filters/http/compressor/mock_compressor_library.proto @@ -4,3 +4,6 @@ package test.mock_compressor_library; message Unregistered { } + +message Registered { +}