diff --git a/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto b/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto index 007ccabc3e47d..e659896fbda89 100644 --- a/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto +++ b/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto @@ -16,6 +16,14 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.http.grpc_json_transcoder] // [#next-free-field: 11] +// GrpcJsonTranscoder filter configuration. +// The filter itself can be used per route / per virtual host or on the general level. The most +// specific one is being used for a given route. If the list of services is empty - filter +// is considered to be disabled. +// Note that if specifying the filter per route, first the route is matched, and then transcoding +// filter is applied. It matters when specifying the route configuration and paths to match the +// request - for per-route grpc transcoder configs, the original path should be matched, while +// in other cases, the grpc-like path is expected (the one AFTER the filter is applied). message GrpcJsonTranscoder { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder"; @@ -80,7 +88,8 @@ message GrpcJsonTranscoder { // the transcoder will translate. If the service name doesn't exist in ``proto_descriptor``, // Envoy will fail at startup. The ``proto_descriptor`` may contain more services than // the service names specified here, but they won't be translated. - repeated string services = 2 [(validate.rules).repeated = {min_items: 1}]; + // If the list of services is empty, filter is considered disabled. + repeated string services = 2; // Control options for response JSON. These options are passed directly to // `JsonPrintOptions ` extension point for custom formatters (command operators). * access log: support command operator: %REQUEST_HEADERS_BYTES%, %RESPONSE_HEADERS_BYTES% and %RESPONSE_TRAILERS_BYTES%. * dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash. +* grpc_json_transcoder: filter can now be configured on per-route/per-vhost level as well. Leaving empty list of services in the filter configuration disables transcoding on the specific route. * http: added support for :ref:`:ref:`preconnecting `. Preconnecting is off by default, but recommended for clusters serving latency-sensitive traffic, especially if using HTTP/1.1. * http: change frame flood and abuse checks to the upstream HTTP/2 codec to ON by default. It can be disabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to false. * overload: add support for scaling :ref:`transport connection timeouts`. This can be used to reduce the TLS handshake timeout in response to overload. diff --git a/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto b/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto index 007ccabc3e47d..e659896fbda89 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto @@ -16,6 +16,14 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.http.grpc_json_transcoder] // [#next-free-field: 11] +// GrpcJsonTranscoder filter configuration. +// The filter itself can be used per route / per virtual host or on the general level. The most +// specific one is being used for a given route. If the list of services is empty - filter +// is considered to be disabled. +// Note that if specifying the filter per route, first the route is matched, and then transcoding +// filter is applied. It matters when specifying the route configuration and paths to match the +// request - for per-route grpc transcoder configs, the original path should be matched, while +// in other cases, the grpc-like path is expected (the one AFTER the filter is applied). message GrpcJsonTranscoder { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder"; @@ -80,7 +88,8 @@ message GrpcJsonTranscoder { // the transcoder will translate. If the service name doesn't exist in ``proto_descriptor``, // Envoy will fail at startup. The ``proto_descriptor`` may contain more services than // the service names specified here, but they won't be translated. - repeated string services = 2 [(validate.rules).repeated = {min_items: 1}]; + // If the list of services is empty, filter is considered disabled. + repeated string services = 2; // Control options for response JSON. These options are passed directly to // `JsonPrintOptions (proto_config, context.api()); +} + /** * Static registration for the grpc transcoding filter. @see RegisterNamedHttpFilterConfigFactory. */ diff --git a/source/extensions/filters/http/grpc_json_transcoder/config.h b/source/extensions/filters/http/grpc_json_transcoder/config.h index eecdcd1bb5cb9..a48b06dd75117 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/config.h +++ b/source/extensions/filters/http/grpc_json_transcoder/config.h @@ -25,6 +25,11 @@ class GrpcJsonTranscoderFilterConfig const envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder& proto_config, const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; + + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder&, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) override; }; } // namespace GrpcJsonTranscoder diff --git a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc index 244e75272d009..1168b63fb0980 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc +++ b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc @@ -17,6 +17,7 @@ #include "common/protobuf/utility.h" #include "extensions/filters/http/grpc_json_transcoder/http_body_utils.h" +#include "extensions/filters/http/well_known_names.h" #include "google/api/annotations.pb.h" #include "google/api/http.pb.h" @@ -111,6 +112,12 @@ JsonTranscoderConfig::JsonTranscoderConfig( const envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder& proto_config, Api::Api& api) { + + disabled_ = proto_config.services().empty(); + if (disabled_) { + return; + } + FileDescriptorSet descriptor_set; switch (proto_config.descriptor_set_case()) { @@ -306,8 +313,11 @@ bool JsonTranscoderConfig::convertGrpcStatus() const { return convert_grpc_statu ProtobufUtil::Status JsonTranscoderConfig::createTranscoder( const Http::RequestHeaderMap& headers, ZeroCopyInputStream& request_input, - google::grpc::transcoding::TranscoderInputStream& response_input, TranscoderPtr& transcoder, - MethodInfoSharedPtr& method_info) { + google::grpc::transcoding::TranscoderInputStream& response_input, + std::unique_ptr& transcoder, MethodInfoSharedPtr& method_info) const { + + ASSERT(!disabled_); + if (Grpc::Common::isGrpcRequestHeaders(headers)) { return ProtobufUtil::Status(Code::INVALID_ARGUMENT, "Request headers has application/grpc content-type"); @@ -384,7 +394,7 @@ ProtobufUtil::Status JsonTranscoderConfig::createTranscoder( ProtobufUtil::Status JsonTranscoderConfig::methodToRequestInfo(const MethodInfoSharedPtr& method_info, - google::grpc::transcoding::RequestInfo* info) { + google::grpc::transcoding::RequestInfo* info) const { const std::string& request_type_full_name = method_info->descriptor_->input_type()->full_name(); auto request_type_url = Grpc::Common::typeUrl(request_type_full_name); info->message_type = type_helper_->Info()->GetTypeByTypeUrl(request_type_url); @@ -399,7 +409,7 @@ JsonTranscoderConfig::methodToRequestInfo(const MethodInfoSharedPtr& method_info ProtobufUtil::Status JsonTranscoderConfig::translateProtoMessageToJson(const Protobuf::Message& message, - std::string* json_out) { + std::string* json_out) const { return ProtobufUtil::BinaryToJsonString( type_helper_->Resolver(), Grpc::Common::typeUrl(message.GetDescriptor()->full_name()), message.SerializeAsString(), json_out, print_options_); @@ -407,10 +417,29 @@ JsonTranscoderConfig::translateProtoMessageToJson(const Protobuf::Message& messa JsonTranscoderFilter::JsonTranscoderFilter(JsonTranscoderConfig& config) : config_(config) {} +void JsonTranscoderFilter::initPerRouteConfig() { + if (!decoder_callbacks_->route() || !decoder_callbacks_->route()->routeEntry()) { + per_route_config_ = &config_; + return; + } + + const std::string& name = HttpFilterNames::get().GrpcJsonTranscoder; + const auto* entry = decoder_callbacks_->route()->routeEntry(); + const auto* route_local = entry->mostSpecificPerFilterConfigTyped(name); + + per_route_config_ = route_local ? route_local : &config_; +} + Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { + + initPerRouteConfig(); + if (per_route_config_->disabled()) { + return Http::FilterHeadersStatus::Continue; + } + const auto status = - config_.createTranscoder(headers, request_in_, response_in_, transcoder_, method_); + per_route_config_->createTranscoder(headers, request_in_, response_in_, transcoder_, method_); if (!status.ok()) { // If transcoder couldn't be created, it might be a normal gRPC request, so the filter will @@ -449,7 +478,7 @@ Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::RequestHeade headers.setReferenceMethod(Http::Headers::get().MethodValues.Post); headers.setReferenceTE(Http::Headers::get().TEValues.Trailers); - if (!config_.matchIncomingRequestInfo()) { + if (!per_route_config_->matchIncomingRequestInfo()) { decoder_callbacks_->clearRouteCache(); } @@ -611,7 +640,7 @@ JsonTranscoderFilter::encodeTrailers(Http::ResponseTrailerMap& trailers) { } void JsonTranscoderFilter::doTrailers(Http::ResponseHeaderOrTrailerMap& headers_or_trailers) { - if (error_ || !transcoder_) { + if (error_ || !transcoder_ || !per_route_config_ || per_route_config_->disabled()) { return; } @@ -767,7 +796,8 @@ bool JsonTranscoderFilter::buildResponseFromHttpBodyOutput( bool JsonTranscoderFilter::maybeConvertGrpcStatus(Grpc::Status::GrpcStatus grpc_status, Http::ResponseHeaderOrTrailerMap& trailers) { - if (!config_.convertGrpcStatus()) { + ASSERT(per_route_config_ && !per_route_config_->disabled()); + if (!per_route_config_->convertGrpcStatus()) { return false; } @@ -799,7 +829,8 @@ bool JsonTranscoderFilter::maybeConvertGrpcStatus(Grpc::Status::GrpcStatus grpc_ } std::string json_status; - auto translate_status = config_.translateProtoMessageToJson(*status_details, &json_status); + auto translate_status = + per_route_config_->translateProtoMessageToJson(*status_details, &json_status); if (!translate_status.ok()) { ENVOY_LOG(debug, "Transcoding status error {}", translate_status.ToString()); return false; diff --git a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h index 2de7578c944e0..0cc9ef860cbcf 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h +++ b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h @@ -56,7 +56,9 @@ void createHttpBodyEnvelope(Buffer::Instance& output, /** * Global configuration for the gRPC JSON transcoder filter. Factory for the Transcoder interface. */ -class JsonTranscoderConfig : public Logger::Loggable { +class JsonTranscoderConfig : public Logger::Loggable, + public Router::RouteSpecificFilterConfig { + public: /** * constructor that loads protobuf descriptors from the file specified in the JSON config. @@ -81,13 +83,13 @@ class JsonTranscoderConfig : public Logger::Loggable { Protobuf::io::ZeroCopyInputStream& request_input, google::grpc::transcoding::TranscoderInputStream& response_input, std::unique_ptr& transcoder, - MethodInfoSharedPtr& method_info); + MethodInfoSharedPtr& method_info) const; /** * Converts an arbitrary protobuf message to JSON. */ ProtobufUtil::Status translateProtoMessageToJson(const Protobuf::Message& message, - std::string* json_out); + std::string* json_out) const; /** * If true, skip clearing the route cache after the incoming request has been modified. @@ -102,12 +104,14 @@ class JsonTranscoderConfig : public Logger::Loggable { */ bool convertGrpcStatus() const; + bool disabled() const { return disabled_; } + private: /** * Convert method descriptor to RequestInfo that needed for transcoding library */ ProtobufUtil::Status methodToRequestInfo(const MethodInfoSharedPtr& method_info, - google::grpc::transcoding::RequestInfo* info); + google::grpc::transcoding::RequestInfo* info) const; private: void addFileDescriptor(const Protobuf::FileDescriptorProto& file); @@ -128,6 +132,8 @@ class JsonTranscoderConfig : public Logger::Loggable { bool match_incoming_request_route_{false}; bool ignore_unknown_query_parameters_{false}; bool convert_grpc_status_{false}; + + bool disabled_; }; using JsonTranscoderConfigSharedPtr = std::shared_ptr; @@ -176,15 +182,17 @@ class JsonTranscoderFilter : public Http::StreamFilter, public Logger::Loggable< Http::ResponseHeaderOrTrailerMap& trailers); bool hasHttpBodyAsOutputType(); void doTrailers(Http::ResponseHeaderOrTrailerMap& headers_or_trailers); + void initPerRouteConfig(); JsonTranscoderConfig& config_; + const JsonTranscoderConfig* per_route_config_{}; std::unique_ptr transcoder_; TranscoderInputStreamImpl request_in_; TranscoderInputStreamImpl response_in_; - Http::StreamDecoderFilterCallbacks* decoder_callbacks_{nullptr}; - Http::StreamEncoderFilterCallbacks* encoder_callbacks_{nullptr}; + Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; MethodInfoSharedPtr method_; - Http::ResponseHeaderMap* response_headers_{nullptr}; + Http::ResponseHeaderMap* response_headers_{}; Grpc::Decoder decoder_; // Data of the initial request message, initialized from query arguments, path, etc. diff --git a/test/extensions/filters/http/grpc_json_transcoder/BUILD b/test/extensions/filters/http/grpc_json_transcoder/BUILD index 0798152a20f75..1c80b34f79568 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/BUILD +++ b/test/extensions/filters/http/grpc_json_transcoder/BUILD @@ -67,6 +67,7 @@ envoy_extension_cc_test( "//test/integration:http_integration_lib", "//test/proto:bookstore_proto_cc_proto", "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/grpc_json_transcoder/v3:pkg_cc_proto", ], ) diff --git a/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc b/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc index a792f8cea8ec0..d0532726d50bc 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/grpc_json_transcoder_integration_test.cc @@ -1,8 +1,12 @@ +#include "envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.pb.h" + #include "common/grpc/codec.h" #include "common/grpc/common.h" #include "common/http/message_impl.h" #include "common/protobuf/protobuf.h" +#include "extensions/filters/http/well_known_names.h" + #include "test/integration/http_integration.h" #include "test/mocks/http/mocks.h" #include "test/proto/bookstore.pb.h" @@ -152,8 +156,28 @@ class GrpcJsonTranscoderIntegrationTest ASSERT_TRUE(fake_upstream_connection_->close()); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } -}; + // override configuration on per-route basis + void overrideConfig(const std::string& json_config) { + + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder per_route_config; + TestUtility::loadFromJson(json_config, per_route_config); + ConfigHelper::HttpModifierFunction modifier = + [per_route_config]( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cfg) { + auto* config = cfg.mutable_route_config() + ->mutable_virtual_hosts() + ->Mutable(0) + ->mutable_typed_per_filter_config(); + + (*config)[Extensions::HttpFilters::HttpFilterNames::get().GrpcJsonTranscoder].PackFrom( + per_route_config); + }; + + config_helper_.addConfigModifier(modifier); + } +}; INSTANTIATE_TEST_SUITE_P(IpVersions, GrpcJsonTranscoderIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); @@ -880,5 +904,68 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, UTF8) { false); } +TEST_P(GrpcJsonTranscoderIntegrationTest, RouteDisabled) { + overrideConfig(R"EOF({"services": [], "proto_descriptor_bin": ""})EOF"); + HttpIntegrationTest::initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, {":path", "/shelves"}, {":authority", "host"}}); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); +}; + +class OverrideConfigGrpcJsonTranscoderIntegrationTest : public GrpcJsonTranscoderIntegrationTest { +public: + /** + * Global initializer for all integration tests. + */ + void SetUp() override { + setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); + // creates filter but doesn't apply it to bookstore services + const std::string filter = + R"EOF( + name: grpc_json_transcoder + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder + "proto_descriptor": "" + )EOF"; + config_helper_.addFilter(filter); + } +}; +INSTANTIATE_TEST_SUITE_P(IpVersions, OverrideConfigGrpcJsonTranscoderIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(OverrideConfigGrpcJsonTranscoderIntegrationTest, RouteOverride) { + // add bookstore per-route override + const std::string filter = + R"EOF({{ + "services": ["bookstore.Bookstore"], + "proto_descriptor": "{}" + }})EOF"; + overrideConfig( + fmt::format(filter, TestEnvironment::runfilesPath("test/proto/bookstore.descriptor"))); + + HttpIntegrationTest::initialize(); + + // testing the path that's defined in bookstore.descriptor file (should work the same way + // as it does when grpc filter is applied to base config) + testTranscoding( + Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, {":path", "/shelves"}, {":authority", "host"}}, + "", {""}, {R"(shelves { id: 20 theme: "Children" } + shelves { id: 1 theme: "Foo" } )"}, + Status(), + Http::TestResponseHeaderMapImpl{{":status", "200"}, + {"content-type", "application/json"}, + {"content-length", "69"}, + {"grpc-status", "0"}}, + R"({"shelves":[{"id":"20","theme":"Children"},{"id":"1","theme":"Foo"}]})"); +}; + } // namespace } // namespace Envoy diff --git a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc index 43e275a5df4de..bc3ad9d3a5af0 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc @@ -11,6 +11,7 @@ #include "common/protobuf/protobuf.h" #include "extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.h" +#include "extensions/filters/http/well_known_names.h" #include "test/mocks/http/mocks.h" #include "test/proto/bookstore.pb.h" @@ -24,6 +25,7 @@ using testing::_; using testing::Invoke; using testing::NiceMock; +using testing::Return; using Envoy::Protobuf::FileDescriptorProto; using Envoy::Protobuf::FileDescriptorSet; @@ -349,6 +351,10 @@ class GrpcJsonTranscoderFilterTest : public testing::Test, public GrpcJsonTransc bookstoreProtoConfig() { const std::string json_string = "{\"proto_descriptor\": \"" + bookstoreDescriptorPath() + "\",\"services\": [\"bookstore.Bookstore\"]}"; + return makeProtoConfig(json_string); + } + static const envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder + makeProtoConfig(const std::string json_string) { envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder proto_config; TestUtility::loadFromJson(json_string, proto_config); return proto_config; @@ -358,6 +364,16 @@ class GrpcJsonTranscoderFilterTest : public testing::Test, public GrpcJsonTransc return TestEnvironment::runfilesPath("test/proto/bookstore.descriptor"); } + void routeLocalConfig(const Router::RouteSpecificFilterConfig* route_settings, + const Router::RouteSpecificFilterConfig* vhost_settings) { + ON_CALL(decoder_callbacks_.route_->route_entry_, + perFilterConfig(HttpFilterNames::get().GrpcJsonTranscoder)) + .WillByDefault(Return(route_settings)); + ON_CALL(decoder_callbacks_.route_->route_entry_.virtual_host_, + perFilterConfig(HttpFilterNames::get().GrpcJsonTranscoder)) + .WillByDefault(Return(vhost_settings)); + } + // TODO(lizan): Add a mock of JsonTranscoderConfig and test more error cases. JsonTranscoderConfig config_; JsonTranscoderFilter filter_; @@ -365,6 +381,41 @@ class GrpcJsonTranscoderFilterTest : public testing::Test, public GrpcJsonTransc NiceMock encoder_callbacks_; }; +TEST_F(GrpcJsonTranscoderFilterTest, EmptyRoute) { + ON_CALL(decoder_callbacks_, route()).WillByDefault(Return(nullptr)); + + Http::TestRequestHeaderMapImpl headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers, false)); +} + +TEST_F(GrpcJsonTranscoderFilterTest, EmptyRouteEntry) { + ON_CALL(*decoder_callbacks_.route_, routeEntry()).WillByDefault(Return(nullptr)); + + Http::TestRequestHeaderMapImpl headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers, false)); +} + +TEST_F(GrpcJsonTranscoderFilterTest, PerRouteDisabledConfigOverride) { + // not setting up services list (which disables filter) + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder route_cfg; + route_cfg.set_proto_descriptor_bin(""); + JsonTranscoderConfig route_config(route_cfg, *api_); + routeLocalConfig(&route_config, nullptr); + + Http::TestRequestHeaderMapImpl headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers, false)); +} + +TEST_F(GrpcJsonTranscoderFilterTest, PerVHostDisabledConfigOverride) { + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder vhost_cfg; + vhost_cfg.set_proto_descriptor_bin(""); + JsonTranscoderConfig vhost_config(vhost_cfg, *api_); + routeLocalConfig(nullptr, &vhost_config); + + Http::TestRequestHeaderMapImpl headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers, false)); +} + TEST_F(GrpcJsonTranscoderFilterTest, NoTranscoding) { Http::TestRequestHeaderMapImpl request_headers{{"content-type", "application/grpc"}, {":method", "POST"}, @@ -1309,6 +1360,54 @@ TEST_P(GrpcJsonTranscoderFilterPrintTest, PrintOptions) { EXPECT_EQ(GetParam().expected_response_, response_json); } +class GrpcJsonTranscoderDisabledFilterTest : public GrpcJsonTranscoderFilterTest { +protected: + GrpcJsonTranscoderDisabledFilterTest() + : GrpcJsonTranscoderFilterTest( + makeProtoConfig("{\"proto_descriptor_bin\": \"\", \"services\": []}")) {} +}; + +TEST_F(GrpcJsonTranscoderDisabledFilterTest, FilterDisabled) { + Http::TestRequestHeaderMapImpl headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers, false)); +} + +TEST_F(GrpcJsonTranscoderDisabledFilterTest, PerRouteEnabledOverride) { + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder route_cfg = + bookstoreProtoConfig(); + JsonTranscoderConfig route_config(route_cfg, *api_); + routeLocalConfig(&route_config, nullptr); + + Http::TestRequestHeaderMapImpl request_headers{ + {"content-type", "application/json"}, {":method", "POST"}, {":path", "/shelf"}}; + + EXPECT_CALL(decoder_callbacks_, clearRouteCache()); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ("application/grpc", request_headers.get_("content-type")); + EXPECT_EQ("/shelf", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("/bookstore.Bookstore/CreateShelf", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); +} + +TEST_F(GrpcJsonTranscoderDisabledFilterTest, PerVhostEnabledOverride) { + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder vhost_cfg = + bookstoreProtoConfig(); + JsonTranscoderConfig vhost_config(vhost_cfg, *api_); + routeLocalConfig(nullptr, &vhost_config); + + Http::TestRequestHeaderMapImpl request_headers{ + {"content-type", "application/json"}, {":method", "POST"}, {":path", "/shelf"}}; + + EXPECT_CALL(decoder_callbacks_, clearRouteCache()); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ("application/grpc", request_headers.get_("content-type")); + EXPECT_EQ("/shelf", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("/bookstore.Bookstore/CreateShelf", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); +} + INSTANTIATE_TEST_SUITE_P( GrpcJsonTranscoderFilterPrintOptions, GrpcJsonTranscoderFilterPrintTest, ::testing::Values(