diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 1ce3c5d3367a0..172717a69ec40 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -11,6 +11,8 @@ Version history * admin: added support for displaying ip address subject alternate names in :ref:`certs` end point. * buffer: force copy when appending small slices to OwnedImpl buffer to avoid fragmentation. * config: use type URL to select an extension whenever the config type URL (or its previous versions) uniquely identify a typed extension, see :ref:`extension configuration `. +* grpc-json: added support for building HTTP request into + `google.api.HttpBody `_. * config: added stat :ref:`update_time `. * datasource: added retry policy for remote async data source. * dns: the STRICT_DNS cluster now only resolves to 0 hosts if DNS resolution successfully returns 0 hosts. diff --git a/source/common/grpc/codec.cc b/source/common/grpc/codec.cc index 1bbb5943b6325..9daa9b1ea5e38 100644 --- a/source/common/grpc/codec.cc +++ b/source/common/grpc/codec.cc @@ -22,6 +22,14 @@ void Encoder::newFrame(uint8_t flags, uint64_t length, std::array& o output[4] = static_cast(length); } +void Encoder::prependFrameHeader(uint8_t flags, Buffer::Instance& buffer) { + // Compute the size of the payload and construct the length prefix. + std::array frame; + Grpc::Encoder().newFrame(flags, buffer.length(), frame); + Buffer::OwnedImpl frame_buffer(frame.data(), frame.size()); + buffer.prepend(frame_buffer); +} + bool Decoder::decode(Buffer::Instance& input, std::vector& output) { decoding_error_ = false; output_ = &output; diff --git a/source/common/grpc/codec.h b/source/common/grpc/codec.h index a0c11ffafc5f0..7577cac5135b0 100644 --- a/source/common/grpc/codec.h +++ b/source/common/grpc/codec.h @@ -32,6 +32,11 @@ class Encoder { // @param length supplies the GRPC data frame length. // @param output the buffer to store the encoded data. Its size must be 5. void newFrame(uint8_t flags, uint64_t length, std::array& output); + + // Prepend the gRPC frame into the buffer. + // @param flags supplies the GRPC data frame flags. + // @param buffer the buffer with the message payload. + void prependFrameHeader(uint8_t flags, Buffer::Instance& buffer); }; // Wire format (http://www.grpc.io/docs/guides/wire.html) of GRPC data frame diff --git a/source/extensions/filters/http/grpc_http1_reverse_bridge/filter.cc b/source/extensions/filters/http/grpc_http1_reverse_bridge/filter.cc index 28371955c0089..d5191716ce595 100644 --- a/source/extensions/filters/http/grpc_http1_reverse_bridge/filter.cc +++ b/source/extensions/filters/http/grpc_http1_reverse_bridge/filter.cc @@ -202,18 +202,12 @@ Http::FilterTrailersStatus Filter::encodeTrailers(Http::ResponseTrailerMap& trai } void Filter::buildGrpcFrameHeader(Buffer::Instance& buffer) { - // Compute the size of the payload and construct the length prefix. - // // We do this even if the upstream failed: If the response returned non-200, // we'll respond with a grpc-status with an error, so clients will know that the request // was unsuccessful. Since we're guaranteed at this point to have a valid response // (unless upstream lied in content-type) we attempt to return a well-formed gRPC // response body. - const auto length = buffer.length(); - std::array frame; - Grpc::Encoder().newFrame(Grpc::GRPC_FH_DEFAULT, length, frame); - Buffer::OwnedImpl frame_buffer(frame.data(), frame.size()); - buffer.prepend(frame_buffer); + Grpc::Encoder().prependFrameHeader(Grpc::GRPC_FH_DEFAULT, buffer); } } // namespace GrpcHttp1ReverseBridge diff --git a/source/extensions/filters/http/grpc_json_transcoder/BUILD b/source/extensions/filters/http/grpc_json_transcoder/BUILD index 344e052e29cec..ca2ce1749d40a 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/BUILD +++ b/source/extensions/filters/http/grpc_json_transcoder/BUILD @@ -23,6 +23,7 @@ envoy_cc_library( "api_httpbody_protos", ], deps = [ + ":http_body_utils_lib", ":transcoder_input_stream_lib", "//include/envoy/http:filter_interface", "//source/common/grpc:codec_lib", @@ -33,6 +34,19 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "http_body_utils_lib", + srcs = ["http_body_utils.cc"], + hdrs = ["http_body_utils.h"], + external_deps = [ + "api_httpbody_protos", + ], + deps = [ + "//source/common/grpc:codec_lib", + "//source/common/protobuf", + ], +) + envoy_cc_library( name = "transcoder_input_stream_lib", srcs = ["transcoder_input_stream_impl.cc"], diff --git a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc new file mode 100644 index 0000000000000..e516a7f2d567e --- /dev/null +++ b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.cc @@ -0,0 +1,73 @@ +#include "extensions/filters/http/grpc_json_transcoder/http_body_utils.h" + +#include "google/api/httpbody.pb.h" + +using Envoy::Protobuf::io::CodedOutputStream; +using Envoy::Protobuf::io::StringOutputStream; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace GrpcJsonTranscoder { + +void HttpBodyUtils::appendHttpBodyEnvelope( + Buffer::Instance& output, const std::vector& request_body_field_path, + std::string content_type, uint64_t content_length) { + // Manually encode the protobuf envelope for the body. + // See https://developers.google.com/protocol-buffers/docs/encoding#embedded for wire format. + + // Embedded messages are treated the same way as strings (wire type 2). + constexpr uint32_t ProtobufLengthDelimitedField = 2; + + std::string proto_envelope; + { + // For memory safety, the StringOutputStream needs to be destroyed before + // we read the string. + + const uint32_t http_body_field_number = + (google::api::HttpBody::kDataFieldNumber << 3) | ProtobufLengthDelimitedField; + + ::google::api::HttpBody body; + body.set_content_type(std::move(content_type)); + + uint64_t envelope_size = body.ByteSizeLong() + + CodedOutputStream::VarintSize32(http_body_field_number) + + CodedOutputStream::VarintSize64(content_length); + std::vector message_sizes; + message_sizes.reserve(request_body_field_path.size()); + for (auto it = request_body_field_path.rbegin(); it != request_body_field_path.rend(); ++it) { + const Protobuf::Field* field = *it; + const uint64_t message_size = envelope_size + content_length; + const uint32_t field_number = (field->number() << 3) | ProtobufLengthDelimitedField; + const uint64_t field_size = CodedOutputStream::VarintSize32(field_number) + + CodedOutputStream::VarintSize64(message_size); + message_sizes.push_back(message_size); + envelope_size += field_size; + } + std::reverse(message_sizes.begin(), message_sizes.end()); + + proto_envelope.reserve(envelope_size); + + Envoy::Protobuf::io::StringOutputStream string_stream(&proto_envelope); + Envoy::Protobuf::io::CodedOutputStream coded_stream(&string_stream); + + // Serialize body field definition manually to avoid the copy of the body. + for (size_t i = 0; i < request_body_field_path.size(); ++i) { + const Protobuf::Field* field = request_body_field_path[i]; + const uint32_t field_number = (field->number() << 3) | ProtobufLengthDelimitedField; + const uint64_t message_size = message_sizes[i]; + coded_stream.WriteTag(field_number); + coded_stream.WriteVarint64(message_size); + } + body.SerializeToCodedStream(&coded_stream); + coded_stream.WriteTag(http_body_field_number); + coded_stream.WriteVarint64(content_length); + } + + output.add(proto_envelope); +} + +} // namespace GrpcJsonTranscoder +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h new file mode 100644 index 0000000000000..dc2af9c3859b4 --- /dev/null +++ b/source/extensions/filters/http/grpc_json_transcoder/http_body_utils.h @@ -0,0 +1,25 @@ +#pragma once + +#include "envoy/buffer/buffer.h" + +#include "common/buffer/buffer_impl.h" +#include "common/grpc/codec.h" +#include "common/protobuf/protobuf.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace GrpcJsonTranscoder { + +class HttpBodyUtils { +public: + static void + appendHttpBodyEnvelope(Buffer::Instance& output, + const std::vector& request_body_field_path, + std::string content_type, uint64_t content_length); +}; + +} // namespace GrpcJsonTranscoder +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy 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 81de9c3e77022..f194bb66ca60e 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 @@ -7,7 +7,6 @@ #include "envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.pb.h" #include "envoy/http/filter.h" -#include "common/buffer/buffer_impl.h" #include "common/common/assert.h" #include "common/common/enum_to_int.h" #include "common/common/utility.h" @@ -17,6 +16,8 @@ #include "common/protobuf/protobuf.h" #include "common/protobuf/utility.h" +#include "extensions/filters/http/grpc_json_transcoder/http_body_utils.h" + #include "google/api/annotations.pb.h" #include "google/api/http.pb.h" #include "google/api/httpbody.pb.h" @@ -25,14 +26,18 @@ #include "grpc_transcoding/response_to_json_translator.h" using Envoy::Protobuf::FileDescriptorSet; +using Envoy::Protobuf::io::CodedOutputStream; +using Envoy::Protobuf::io::StringOutputStream; using Envoy::Protobuf::io::ZeroCopyInputStream; using Envoy::ProtobufUtil::Status; using Envoy::ProtobufUtil::error::Code; using google::api::HttpRule; using google::grpc::transcoding::JsonRequestTranslator; +using google::grpc::transcoding::MessageStream; using google::grpc::transcoding::PathMatcherBuilder; using google::grpc::transcoding::PathMatcherUtility; using google::grpc::transcoding::RequestInfo; +using google::grpc::transcoding::RequestMessageTranslator; using google::grpc::transcoding::ResponseToJsonTranslator; using google::grpc::transcoding::Transcoder; using google::grpc::transcoding::TranscoderInputStream; @@ -68,24 +73,30 @@ class TranscoderImpl : public Transcoder { * @param request_translator a JsonRequestTranslator that does the request translation * @param response_translator a ResponseToJsonTranslator that does the response translation */ - TranscoderImpl(std::unique_ptr request_translator, + TranscoderImpl(std::unique_ptr request_translator, + std::unique_ptr json_request_translator, std::unique_ptr response_translator) : request_translator_(std::move(request_translator)), + json_request_translator_(std::move(json_request_translator)), + request_message_stream_(request_translator_ ? *request_translator_ + : json_request_translator_->Output()), response_translator_(std::move(response_translator)), - request_stream_(request_translator_->Output().CreateInputStream()), + request_stream_(request_message_stream_.CreateInputStream()), response_stream_(response_translator_->CreateInputStream()) {} // Transcoder ::google::grpc::transcoding::TranscoderInputStream* RequestOutput() override { return request_stream_.get(); } - ProtobufUtil::Status RequestStatus() override { return request_translator_->Output().Status(); } + ProtobufUtil::Status RequestStatus() override { return request_message_stream_.Status(); } ZeroCopyInputStream* ResponseOutput() override { return response_stream_.get(); } ProtobufUtil::Status ResponseStatus() override { return response_translator_->Status(); } private: - std::unique_ptr request_translator_; + std::unique_ptr request_translator_; + std::unique_ptr json_request_translator_; + MessageStream& request_message_stream_; std::unique_ptr response_translator_; std::unique_ptr request_stream_; std::unique_ptr response_stream_; @@ -127,7 +138,11 @@ JsonTranscoderConfig::JsonTranscoderConfig( addBuiltinSymbolDescriptor("google.rpc.Status"); } - PathMatcherBuilder pmb; + type_helper_ = std::make_unique( + Protobuf::util::NewTypeResolverForDescriptorPool(Grpc::Common::typeUrlPrefix(), + &descriptor_pool_)); + + PathMatcherBuilder pmb; std::unordered_set ignored_query_parameters; for (const auto& query_param : proto_config.ignored_query_parameters()) { ignored_query_parameters.insert(query_param); @@ -151,8 +166,15 @@ JsonTranscoderConfig::JsonTranscoderConfig( http_rule.set_body("*"); } + MethodInfoSharedPtr method_info; + Status status = createMethodInfo(method, http_rule, method_info); + if (!status.ok()) { + throw EnvoyException("transcoding_filter: Cannot register '" + method->full_name() + + "': " + status.message().ToString()); + } + if (!PathMatcherUtility::RegisterByHttpRule(pmb, http_rule, ignored_query_parameters, - method)) { + method_info)) { throw EnvoyException("transcoding_filter: Cannot register '" + method->full_name() + "' to path matcher"); } @@ -161,10 +183,6 @@ JsonTranscoderConfig::JsonTranscoderConfig( path_matcher_ = pmb.Build(); - type_helper_ = std::make_unique( - Protobuf::util::NewTypeResolverForDescriptorPool(Grpc::Common::typeUrlPrefix(), - &descriptor_pool_)); - const auto& print_config = proto_config.print_options(); print_options_.add_whitespace = print_config.add_whitespace(); print_options_.always_print_primitive_fields = print_config.always_print_primitive_fields(); @@ -197,6 +215,42 @@ void JsonTranscoderConfig::addBuiltinSymbolDescriptor(const std::string& symbol_ addFileDescriptor(file_proto); } +Status JsonTranscoderConfig::createMethodInfo(const Protobuf::MethodDescriptor* descriptor, + const HttpRule& http_rule, + MethodInfoSharedPtr& method_info) { + method_info = std::make_shared(); + method_info->descriptor_ = descriptor; + method_info->response_type_is_http_body_ = + descriptor->output_type()->full_name() == google::api::HttpBody::descriptor()->full_name(); + + const Protobuf::Type* request_type = type_helper_->Info()->GetTypeByTypeUrl( + Grpc::Common::typeUrl(descriptor->input_type()->full_name())); + if (request_type == nullptr) { + return ProtobufUtil::Status(Code::NOT_FOUND, + "Could not resolve type: " + descriptor->input_type()->full_name()); + } + + Status status = + type_helper_->ResolveFieldPath(*request_type, http_rule.body() == "*" ? "" : http_rule.body(), + &method_info->request_body_field_path); + if (!status.ok()) { + return status; + } + + if (method_info->request_body_field_path.empty()) { + method_info->request_type_is_http_body_ = + descriptor->input_type()->full_name() == google::api::HttpBody::descriptor()->full_name(); + } else { + const Protobuf::Type* body_type = type_helper_->Info()->GetTypeByTypeUrl( + method_info->request_body_field_path.back()->type_url()); + method_info->request_type_is_http_body_ = + body_type != nullptr && + body_type->name() == google::api::HttpBody::descriptor()->full_name(); + } + + return Status::OK; +} + bool JsonTranscoderConfig::matchIncomingRequestInfo() const { return match_incoming_request_route_; } @@ -206,7 +260,7 @@ 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, - std::unique_ptr& transcoder, const Protobuf::MethodDescriptor*& method_descriptor) { + std::unique_ptr& transcoder, MethodInfoSharedPtr& method_info) { if (Grpc::Common::hasGrpcContentType(headers)) { return ProtobufUtil::Status(Code::INVALID_ARGUMENT, "Request headers has application/grpc content-type"); @@ -223,13 +277,13 @@ ProtobufUtil::Status JsonTranscoderConfig::createTranscoder( struct RequestInfo request_info; std::vector variable_bindings; - method_descriptor = + method_info = path_matcher_->Lookup(method, path, args, &variable_bindings, &request_info.body_field_path); - if (!method_descriptor) { + if (!method_info) { return ProtobufUtil::Status(Code::NOT_FOUND, "Could not resolve " + path + " to a method"); } - auto status = methodToRequestInfo(method_descriptor, &request_info); + auto status = methodToRequestInfo(method_info, &request_info); if (!status.ok()) { return status; } @@ -250,30 +304,40 @@ ProtobufUtil::Status JsonTranscoderConfig::createTranscoder( request_info.variable_bindings.emplace_back(std::move(resolved_binding)); } - std::unique_ptr request_translator{ - new JsonRequestTranslator(type_helper_->Resolver(), &request_input, request_info, - method_descriptor->client_streaming(), true)}; + std::unique_ptr request_translator; + std::unique_ptr json_request_translator; + if (method_info->request_type_is_http_body_) { + request_translator = std::make_unique(*type_helper_->Resolver(), + false, std::move(request_info)); + request_translator->Input().StartObject(nullptr)->EndObject(); + } else { + json_request_translator = std::make_unique( + type_helper_->Resolver(), &request_input, std::move(request_info), + method_info->descriptor_->client_streaming(), true); + } const auto response_type_url = - Grpc::Common::typeUrl(method_descriptor->output_type()->full_name()); + Grpc::Common::typeUrl(method_info->descriptor_->output_type()->full_name()); std::unique_ptr response_translator{new ResponseToJsonTranslator( - type_helper_->Resolver(), response_type_url, method_descriptor->server_streaming(), + type_helper_->Resolver(), response_type_url, method_info->descriptor_->server_streaming(), &response_input, print_options_)}; transcoder = std::make_unique(std::move(request_translator), + std::move(json_request_translator), std::move(response_translator)); return ProtobufUtil::Status(); } ProtobufUtil::Status -JsonTranscoderConfig::methodToRequestInfo(const Protobuf::MethodDescriptor* method, +JsonTranscoderConfig::methodToRequestInfo(const MethodInfoSharedPtr& method_info, google::grpc::transcoding::RequestInfo* info) { - auto request_type_url = Grpc::Common::typeUrl(method->input_type()->full_name()); + 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); if (info->message_type == nullptr) { - ENVOY_LOG(debug, "Cannot resolve input-type: {}", method->input_type()->full_name()); + ENVOY_LOG(debug, "Cannot resolve input-type: {}", request_type_full_name); return ProtobufUtil::Status(Code::NOT_FOUND, - "Could not resolve type: " + method->input_type()->full_name()); + "Could not resolve type: " + request_type_full_name); } return ProtobufUtil::Status(); @@ -299,12 +363,35 @@ Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::RequestHeade // just pass-through the request to upstream. return Http::FilterHeadersStatus::Continue; } - has_http_body_output_ = !method_->server_streaming() && hasHttpBodyAsOutputType(); + has_http_body_response_ = + !method_->descriptor_->server_streaming() && method_->response_type_is_http_body_; + if (method_->request_type_is_http_body_) { + if (headers.ContentType() != nullptr) { + absl::string_view content_type = headers.ContentType()->value().getStringView(); + content_type_.assign(content_type.begin(), content_type.end()); + } + + bool done = !readToBuffer(*transcoder_->RequestOutput(), initial_request_data_); + if (!done) { + ENVOY_LOG( + debug, + "Transcoding of query arguments of HttpBody request is not done (unexpected state)"); + error_ = true; + decoder_callbacks_->sendLocalReply( + Http::Code::BadRequest, "Bad request", nullptr, absl::nullopt, + absl::StrCat(RcDetails::get().GrpcTranscodeFailedEarly, "{BAD_REQUEST}")); + return Http::FilterHeadersStatus::StopIteration; + } + if (checkIfTranscoderFailed(RcDetails::get().GrpcTranscodeFailed)) { + return Http::FilterHeadersStatus::StopIteration; + } + } headers.removeContentLength(); headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Grpc); headers.setEnvoyOriginalPath(headers.Path()->value().getStringView()); - headers.setPath("/" + method_->service()->full_name() + "/" + method_->name()); + headers.setPath("/" + method_->descriptor_->service()->full_name() + "/" + + method_->descriptor_->name()); headers.setReferenceMethod(Http::Headers::get().MethodValues.Post); headers.setReferenceTE(Http::Headers::get().TEValues.Trailers); @@ -312,21 +399,12 @@ Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::RequestHeade decoder_callbacks_->clearRouteCache(); } - if (end_stream) { + if (end_stream && method_->request_type_is_http_body_) { + maybeSendHttpBodyRequestMessage(); + } else if (end_stream) { request_in_.finish(); - const auto& request_status = transcoder_->RequestStatus(); - if (!request_status.ok()) { - ENVOY_LOG(debug, "Transcoding request error {}", request_status.ToString()); - error_ = true; - decoder_callbacks_->sendLocalReply( - Http::Code::BadRequest, - absl::string_view(request_status.error_message().data(), - request_status.error_message().size()), - nullptr, absl::nullopt, - absl::StrCat(RcDetails::get().GrpcTranscodeFailedEarly, "{", - MessageUtil::CodeEnumToString(request_status.code()), "}")); - + if (checkIfTranscoderFailed(RcDetails::get().GrpcTranscodeFailedEarly)) { return Http::FilterHeadersStatus::StopIteration; } @@ -347,27 +425,26 @@ Http::FilterDataStatus JsonTranscoderFilter::decodeData(Buffer::Instance& data, return Http::FilterDataStatus::Continue; } - request_in_.move(data); - - if (end_stream) { - request_in_.finish(); - } - - readToBuffer(*transcoder_->RequestOutput(), data); + if (method_->request_type_is_http_body_) { + request_data_.move(data); + // TODO(euroelessar): Upper bound message size for streaming case. + if (end_stream || method_->descriptor_->client_streaming()) { + maybeSendHttpBodyRequestMessage(); + } else { + // TODO(euroelessar): Avoid buffering if content length is already known. + return Http::FilterDataStatus::StopIterationAndBuffer; + } + } else { + request_in_.move(data); - const auto& request_status = transcoder_->RequestStatus(); + if (end_stream) { + request_in_.finish(); + } - if (!request_status.ok()) { - ENVOY_LOG(debug, "Transcoding request error {}", request_status.ToString()); - error_ = true; - decoder_callbacks_->sendLocalReply( - Http::Code::BadRequest, - absl::string_view(request_status.error_message().data(), - request_status.error_message().size()), - nullptr, absl::nullopt, - absl::StrCat(RcDetails::get().GrpcTranscodeFailed, "{", - MessageUtil::CodeEnumToString(request_status.code()), "}")); + readToBuffer(*transcoder_->RequestOutput(), data); + } + if (checkIfTranscoderFailed(RcDetails::get().GrpcTranscodeFailed)) { return Http::FilterDataStatus::StopIterationNoBuffer; } return Http::FilterDataStatus::Continue; @@ -380,13 +457,17 @@ Http::FilterTrailersStatus JsonTranscoderFilter::decodeTrailers(Http::RequestTra return Http::FilterTrailersStatus::Continue; } - request_in_.finish(); + if (method_->request_type_is_http_body_) { + maybeSendHttpBodyRequestMessage(); + } else { + request_in_.finish(); - Buffer::OwnedImpl data; - readToBuffer(*transcoder_->RequestOutput(), data); + Buffer::OwnedImpl data; + readToBuffer(*transcoder_->RequestOutput(), data); - if (data.length()) { - decoder_callbacks_->addDecodedData(data, true); + if (data.length()) { + decoder_callbacks_->addDecodedData(data, true); + } } return Http::FilterTrailersStatus::Continue; } @@ -409,7 +490,7 @@ Http::FilterHeadersStatus JsonTranscoderFilter::encodeHeaders(Http::ResponseHead response_headers_ = &headers; if (end_stream) { - if (method_->server_streaming()) { + if (method_->descriptor_->server_streaming()) { // When there is no body in a streaming response, a empty JSON array is // returned by default. Set the content type correctly. headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); @@ -423,7 +504,7 @@ Http::FilterHeadersStatus JsonTranscoderFilter::encodeHeaders(Http::ResponseHead } headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json); - if (!method_->server_streaming()) { + if (!method_->descriptor_->server_streaming()) { return Http::FilterHeadersStatus::StopIteration; } @@ -438,7 +519,7 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, has_body_ = true; // TODO(dio): Add support for streaming case. - if (has_http_body_output_) { + if (has_http_body_response_) { buildResponseFromHttpBodyOutput(*response_headers_, data); return Http::FilterDataStatus::StopIterationAndBuffer; } @@ -451,7 +532,7 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, readToBuffer(*transcoder_->ResponseOutput(), data); - if (!method_->server_streaming() && !end_stream) { + if (!method_->descriptor_->server_streaming() && !end_stream) { // Buffer until the response is complete. return Http::FilterDataStatus::StopIterationAndBuffer; } @@ -480,7 +561,7 @@ void JsonTranscoderFilter::doTrailers(Http::ResponseHeaderOrTrailerMap& headers_ encoder_callbacks_->addEncodedData(data, true); } - if (method_->server_streaming()) { + if (method_->descriptor_->server_streaming()) { // For streaming case, the headers are already sent, so just continue here. return; } @@ -520,6 +601,23 @@ void JsonTranscoderFilter::setEncoderFilterCallbacks( encoder_callbacks_ = &callbacks; } +bool JsonTranscoderFilter::checkIfTranscoderFailed(const std::string& details) { + const auto& request_status = transcoder_->RequestStatus(); + if (!request_status.ok()) { + ENVOY_LOG(debug, "Transcoding request error {}", request_status.ToString()); + error_ = true; + decoder_callbacks_->sendLocalReply( + Http::Code::BadRequest, + absl::string_view(request_status.error_message().data(), + request_status.error_message().size()), + nullptr, absl::nullopt, + absl::StrCat(details, "{", MessageUtil::CodeEnumToString(request_status.code()), "}")); + + return true; + } + return false; +} + // TODO(lizan): Incorporate watermarks to bound buffer sizes bool JsonTranscoderFilter::readToBuffer(Protobuf::io::ZeroCopyInputStream& stream, Buffer::Instance& data) { @@ -535,6 +633,25 @@ bool JsonTranscoderFilter::readToBuffer(Protobuf::io::ZeroCopyInputStream& strea return false; } +void JsonTranscoderFilter::maybeSendHttpBodyRequestMessage() { + if (first_request_sent_ && request_data_.length() == 0) { + return; + } + + Buffer::OwnedImpl message_payload; + message_payload.move(initial_request_data_); + HttpBodyUtils::appendHttpBodyEnvelope(message_payload, method_->request_body_field_path, + std::move(content_type_), request_data_.length()); + content_type_.clear(); + message_payload.move(request_data_); + + Envoy::Grpc::Encoder().prependFrameHeader(Envoy::Grpc::GRPC_FH_DEFAULT, message_payload); + + decoder_callbacks_->addDecodedData(message_payload, true); + + first_request_sent_ = true; +} + void JsonTranscoderFilter::buildResponseFromHttpBodyOutput( Http::ResponseHeaderMap& response_headers, Buffer::Instance& data) { std::vector frames; @@ -620,10 +737,6 @@ bool JsonTranscoderFilter::maybeConvertGrpcStatus(Grpc::Status::GrpcStatus grpc_ return true; } -bool JsonTranscoderFilter::hasHttpBodyAsOutputType() { - return method_->output_type()->full_name() == google::api::HttpBody::descriptor()->full_name(); -} - } // namespace GrpcJsonTranscoder } // namespace HttpFilters } // namespace Extensions 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 8d43c41cc107e..354b0b010230c 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 @@ -7,6 +7,7 @@ #include "envoy/http/header_map.h" #include "envoy/json/json_object.h" +#include "common/buffer/buffer_impl.h" #include "common/common/logger.h" #include "common/grpc/codec.h" #include "common/protobuf/protobuf.h" @@ -40,6 +41,18 @@ struct VariableBinding { std::string value; }; +struct MethodInfo { + const Protobuf::MethodDescriptor* descriptor_ = nullptr; + std::vector request_body_field_path; + bool request_type_is_http_body_ = false; + bool response_type_is_http_body_ = false; +}; +typedef std::shared_ptr MethodInfoSharedPtr; + +void createHttpBodyEnvelope(Buffer::Instance& output, + const std::vector& request_body_field_path, + std::string content_type, uint64_t content_length); + /** * Global configuration for the gRPC JSON transcoder filter. Factory for the Transcoder interface. */ @@ -68,7 +81,7 @@ class JsonTranscoderConfig : public Logger::Loggable { Protobuf::io::ZeroCopyInputStream& request_input, google::grpc::transcoding::TranscoderInputStream& response_input, std::unique_ptr& transcoder, - const Protobuf::MethodDescriptor*& method_descriptor); + MethodInfoSharedPtr& method_info); /** * Converts an arbitrary protobuf message to JSON. @@ -93,15 +106,18 @@ class JsonTranscoderConfig : public Logger::Loggable { /** * Convert method descriptor to RequestInfo that needed for transcoding library */ - ProtobufUtil::Status methodToRequestInfo(const Protobuf::MethodDescriptor* method, + ProtobufUtil::Status methodToRequestInfo(const MethodInfoSharedPtr& method_info, google::grpc::transcoding::RequestInfo* info); private: void addFileDescriptor(const Protobuf::FileDescriptorProto& file); void addBuiltinSymbolDescriptor(const std::string& symbol_name); + ProtobufUtil::Status createMethodInfo(const Protobuf::MethodDescriptor* descriptor, + const google::api::HttpRule& http_rule, + MethodInfoSharedPtr& method_info); Protobuf::DescriptorPool descriptor_pool_; - google::grpc::transcoding::PathMatcherPtr path_matcher_; + google::grpc::transcoding::PathMatcherPtr path_matcher_; std::unique_ptr type_helper_; Protobuf::util::JsonPrintOptions print_options_; @@ -146,7 +162,9 @@ class JsonTranscoderFilter : public Http::StreamFilter, public Logger::Loggable< void onDestroy() override {} private: + bool checkIfTranscoderFailed(const std::string& details); bool readToBuffer(Protobuf::io::ZeroCopyInputStream& stream, Buffer::Instance& data); + void maybeSendHttpBodyRequestMessage(); void buildResponseFromHttpBodyOutput(Http::ResponseHeaderMap& response_headers, Buffer::Instance& data); bool maybeConvertGrpcStatus(Grpc::Status::GrpcStatus grpc_status, @@ -160,12 +178,18 @@ class JsonTranscoderFilter : public Http::StreamFilter, public Logger::Loggable< TranscoderInputStreamImpl response_in_; Http::StreamDecoderFilterCallbacks* decoder_callbacks_{nullptr}; Http::StreamEncoderFilterCallbacks* encoder_callbacks_{nullptr}; - const Protobuf::MethodDescriptor* method_{nullptr}; + MethodInfoSharedPtr method_; Http::ResponseHeaderMap* response_headers_{nullptr}; Grpc::Decoder decoder_; + // Data of the initial request message, initialized from query arguments, path, etc. + Buffer::OwnedImpl initial_request_data_; + Buffer::OwnedImpl request_data_; + bool first_request_sent_{false}; + std::string content_type_; + bool error_{false}; - bool has_http_body_output_{false}; + bool has_http_body_response_{false}; bool has_body_{false}; }; diff --git a/test/extensions/filters/http/grpc_json_transcoder/BUILD b/test/extensions/filters/http/grpc_json_transcoder/BUILD index b6a407573d5a4..977669d16fd8e 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/BUILD +++ b/test/extensions/filters/http/grpc_json_transcoder/BUILD @@ -30,6 +30,18 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "http_body_utils_test", + srcs = ["http_body_utils_test.cc"], + extension_name = "envoy.filters.http.grpc_json_transcoder", + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/buffer:zero_copy_input_stream_lib", + "//source/extensions/filters/http/grpc_json_transcoder:http_body_utils_lib", + "//test/proto:bookstore_proto_cc_proto", + ], +) + envoy_extension_cc_test( name = "transcoder_input_stream_test", srcs = ["transcoder_input_stream_test.cc"], 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 9add713977585..4b6cd69535df9 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 @@ -81,9 +81,15 @@ class GrpcJsonTranscoderIntegrationTest ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); + std::string dump; + for (char ch : upstream_request_->body().toString()) { + dump += std::to_string(int(ch)); + dump += " "; + } + Grpc::Decoder grpc_decoder; std::vector frames; - EXPECT_TRUE(grpc_decoder.decode(upstream_request_->body(), frames)); + EXPECT_TRUE(grpc_decoder.decode(upstream_request_->body(), frames)) << dump; EXPECT_EQ(grpc_request_messages.size(), frames.size()); for (size_t i = 0; i < grpc_request_messages.size(); ++i) { @@ -93,8 +99,7 @@ class GrpcJsonTranscoderIntegrationTest } RequestType expected_message; EXPECT_TRUE(TextFormat::ParseFromString(grpc_request_messages[i], &expected_message)); - - EXPECT_TRUE(MessageDifferencer::Equivalent(expected_message, actual_message)); + EXPECT_THAT(actual_message, ProtoEq(expected_message)); } Http::TestResponseHeaderMapImpl response_headers; @@ -323,6 +328,22 @@ TEST_P(GrpcJsonTranscoderIntegrationTest, UnaryGetHttpBody) { R"(

Hello!

)"); } +TEST_P(GrpcJsonTranscoderIntegrationTest, UnaryEchoHttpBody) { + HttpIntegrationTest::initialize(); + testTranscoding( + Http::TestRequestHeaderMapImpl{{":method", "POST"}, + {":path", "/echoBody?arg=oops"}, + {":authority", "host"}, + {"content-type", "text/plain"}}, + "Hello!", {R"(arg: "oops" nested { content { content_type: "text/plain" data: "Hello!" } })"}, + {R"(content_type: "text/html" data: "

Hello!

" )"}, Status(), + Http::TestResponseHeaderMapImpl{{":status", "200"}, + {"content-type", "text/html"}, + {"content-length", "15"}, + {"grpc-status", "0"}}, + R"(

Hello!

)"); +} + TEST_P(GrpcJsonTranscoderIntegrationTest, UnaryGetError) { HttpIntegrationTest::initialize(); testTranscoding( diff --git a/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc b/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc new file mode 100644 index 0000000000000..f89d0f66936f4 --- /dev/null +++ b/test/extensions/filters/http/grpc_json_transcoder/http_body_utils_test.cc @@ -0,0 +1,84 @@ +#include "common/buffer/buffer_impl.h" +#include "common/buffer/zero_copy_input_stream_impl.h" + +#include "extensions/filters/http/grpc_json_transcoder/http_body_utils.h" + +#include "test/proto/bookstore.pb.h" + +#include "google/api/httpbody.pb.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace GrpcJsonTranscoder { +namespace { + +class HttpBodyUtilsTest : public testing::Test { +public: + HttpBodyUtilsTest() {} + + template + void basicTest(const std::string& content, const std::string& content_type, + const std::vector& body_field_path, + std::function get_http_body) { + for (int field_number : body_field_path) { + Protobuf::Field field; + field.set_number(field_number); + raw_body_field_path_.emplace_back(std::move(field)); + } + for (auto& field : raw_body_field_path_) { + body_field_path_.push_back(&field); + } + + Buffer::InstancePtr message_buffer = std::make_unique(); + HttpBodyUtils::appendHttpBodyEnvelope(*message_buffer, body_field_path_, content_type, + content.length()); + message_buffer->add(content); + + Buffer::ZeroCopyInputStreamImpl stream(std::move(message_buffer)); + + Message message; + message.ParseFromZeroCopyStream(&stream); + + google::api::HttpBody http_body = get_http_body(std::move(message)); + EXPECT_EQ(http_body.content_type(), content_type); + EXPECT_EQ(http_body.data(), content); + } + + std::vector raw_body_field_path_; + std::vector body_field_path_; +}; + +TEST_F(HttpBodyUtilsTest, EmptyFieldsList) { + basicTest("abcd", "text/plain", {}, + [](google::api::HttpBody http_body) { return http_body; }); +} + +TEST_F(HttpBodyUtilsTest, LargeMessage) { + // Check some content with more than single byte in varint encoding of the size. + std::string content; + content.assign(20000, 'a'); + basicTest(content, "text/binary", {}, + [](google::api::HttpBody http_body) { return http_body; }); +} + +TEST_F(HttpBodyUtilsTest, LargeContentType) { + // Check some content type with more than single byte in varint encoding of the size. + std::string content_type; + content_type.assign(20000, 'a'); + basicTest("abcd", content_type, {}, + [](google::api::HttpBody http_body) { return http_body; }); +} + +TEST_F(HttpBodyUtilsTest, NestedFieldsList) { + basicTest( + "abcd", "text/nested", {1, 1000000, 100000000, 500000000}, + [](bookstore::DeepNestedBody message) { return message.nested().nested().nested().body(); }); +} + +} // namespace +} // namespace GrpcJsonTranscoder +} // namespace HttpFilters +} // namespace Extensions +} // 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 6c7629a367667..613b9acaa7a26 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 @@ -198,13 +198,13 @@ TEST_F(GrpcJsonTranscoderConfigTest, CreateTranscoder) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_TRUE(status.ok()); EXPECT_TRUE(transcoder); - EXPECT_EQ("bookstore.Bookstore.ListShelves", method_descriptor->full_name()); + EXPECT_EQ("bookstore.Bookstore.ListShelves", method_info->descriptor_->full_name()); } TEST_F(GrpcJsonTranscoderConfigTest, CreateTranscoderAutoMap) { @@ -219,13 +219,13 @@ TEST_F(GrpcJsonTranscoderConfigTest, CreateTranscoderAutoMap) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_TRUE(status.ok()); EXPECT_TRUE(transcoder); - EXPECT_EQ("bookstore.Bookstore.DeleteShelf", method_descriptor->full_name()); + EXPECT_EQ("bookstore.Bookstore.DeleteShelf", method_info->descriptor_->full_name()); } TEST_F(GrpcJsonTranscoderConfigTest, InvalidQueryParameter) { @@ -238,9 +238,9 @@ TEST_F(GrpcJsonTranscoderConfigTest, InvalidQueryParameter) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_EQ(Code::INVALID_ARGUMENT, status.error_code()); EXPECT_EQ("Could not find field \"foo\" in the type \"google.protobuf.Empty\".", @@ -258,9 +258,9 @@ TEST_F(GrpcJsonTranscoderConfigTest, UnknownQueryParameterIsIgnored) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_TRUE(status.ok()); EXPECT_TRUE(transcoder); @@ -277,13 +277,13 @@ TEST_F(GrpcJsonTranscoderConfigTest, IgnoredQueryParameter) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_TRUE(status.ok()); EXPECT_TRUE(transcoder); - EXPECT_EQ("bookstore.Bookstore.ListShelves", method_descriptor->full_name()); + EXPECT_EQ("bookstore.Bookstore.ListShelves", method_info->descriptor_->full_name()); } TEST_F(GrpcJsonTranscoderConfigTest, InvalidVariableBinding) { @@ -299,9 +299,9 @@ TEST_F(GrpcJsonTranscoderConfigTest, InvalidVariableBinding) { TranscoderInputStreamImpl request_in, response_in; std::unique_ptr transcoder; - const MethodDescriptor* method_descriptor; + MethodInfoSharedPtr method_info; const auto status = - config.createTranscoder(headers, request_in, response_in, transcoder, method_descriptor); + config.createTranscoder(headers, request_in, response_in, transcoder, method_info); EXPECT_EQ(Code::INVALID_ARGUMENT, status.error_code()); EXPECT_EQ("Could not find field \"b\" in the type \"bookstore.GetBookRequest\".", @@ -746,6 +746,109 @@ TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryWithHttpBodyAsOutputAndSpli EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_trailers)); } +TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryPostWithHttpBody) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/postBody?arg=hi"}, {"content-type", "text/plain"}}; + + 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("/postBody?arg=hi", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("/bookstore.Bookstore/PostBody", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); + + Grpc::Decoder decoder; + std::vector frames; + + EXPECT_CALL(decoder_callbacks_, addDecodedData(_, true)) + .Times(testing::AtLeast(1)) + .WillRepeatedly(testing::Invoke([&decoder, &frames](Buffer::Instance& data, bool end_stream) { + EXPECT_TRUE(end_stream); + decoder.decode(data, frames); + })); + + Buffer::OwnedImpl buffer; + buffer.add("hello"); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter_.decodeData(buffer, false)); + EXPECT_EQ(buffer.length(), 0); + EXPECT_EQ(frames.size(), 0); + buffer.add(" "); + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter_.decodeData(buffer, false)); + EXPECT_EQ(buffer.length(), 0); + EXPECT_EQ(frames.size(), 0); + buffer.add("world!"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(buffer, true)); + EXPECT_EQ(buffer.length(), 0); + ASSERT_EQ(frames.size(), 1); + + bookstore::EchoBodyRequest expected_request; + expected_request.set_arg("hi"); + expected_request.mutable_nested()->mutable_content()->set_content_type("text/plain"); + expected_request.mutable_nested()->mutable_content()->set_data("hello world!"); + + bookstore::EchoBodyRequest request; + request.ParseFromString(frames[0].data_->toString()); + + EXPECT_THAT(request, ProtoEq(expected_request)); +} + +TEST_F(GrpcJsonTranscoderFilterTest, TranscodingStreamPostWithHttpBody) { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/streamBody?arg=hi"}, {"content-type", "text/plain"}}; + + 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("/streamBody?arg=hi", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("/bookstore.Bookstore/StreamBody", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); + + Grpc::Decoder decoder; + std::vector frames; + + EXPECT_CALL(decoder_callbacks_, addDecodedData(_, true)) + .Times(testing::AtLeast(2)) + .WillRepeatedly(testing::Invoke([&decoder, &frames](Buffer::Instance& data, bool end_stream) { + EXPECT_TRUE(end_stream); + decoder.decode(data, frames); + })); + + Buffer::OwnedImpl buffer; + buffer.add("hello"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(buffer, false)); + EXPECT_EQ(buffer.length(), 0); + EXPECT_EQ(frames.size(), 1); + buffer.add(" "); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(buffer, false)); + EXPECT_EQ(buffer.length(), 0); + EXPECT_EQ(frames.size(), 2); + buffer.add("world!"); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(buffer, true)); + EXPECT_EQ(buffer.length(), 0); + ASSERT_EQ(frames.size(), 3); + + bookstore::EchoBodyRequest expected_request; + bookstore::EchoBodyRequest request; + + expected_request.set_arg("hi"); + expected_request.mutable_nested()->mutable_content()->set_content_type("text/plain"); + expected_request.mutable_nested()->mutable_content()->set_data("hello"); + request.ParseFromString(frames[0].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_request)); + + expected_request.Clear(); + expected_request.mutable_nested()->mutable_content()->set_data(" "); + request.ParseFromString(frames[1].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_request)); + + expected_request.Clear(); + expected_request.mutable_nested()->mutable_content()->set_data("world!"); + request.ParseFromString(frames[2].data_->toString()); + EXPECT_THAT(request, ProtoEq(expected_request)); +} + class GrpcJsonTranscoderFilterGrpcStatusTest : public GrpcJsonTranscoderFilterTest { public: GrpcJsonTranscoderFilterGrpcStatusTest( diff --git a/test/proto/bookstore.proto b/test/proto/bookstore.proto index fc632de0c1bac..42c180ae734dd 100644 --- a/test/proto/bookstore.proto +++ b/test/proto/bookstore.proto @@ -91,6 +91,24 @@ service Bookstore { get: "/index" }; } + rpc PostBody(EchoBodyRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/postBody" + body: "nested.content" + }; + } + rpc StreamBody(stream EchoBodyRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/streamBody" + body: "nested.content" + }; + } + rpc EchoBody(EchoBodyRequest) returns (google.api.HttpBody) { + option (google.api.http) = { + post: "/echoBody" + body: "nested.content" + }; + } rpc EchoStruct(EchoStructReqResp) returns (EchoStructReqResp) { option (google.api.http) = { post: "/echoStruct" @@ -201,8 +219,31 @@ message GetAuthorRequest { int64 author = 1; } +message EchoBodyRequest { + message Nested { + google.api.HttpBody content = 1; + } + string arg = 1; + string unused = 2; + Nested nested = 3; +} + // Request and Response message for EchoStructReqResp method. message EchoStructReqResp { // The content of request. google.protobuf.Struct content = 1; } + +// Test message for deeply-nested HttpBody field. +message DeepNestedBody { + message Nested { + message Nested { + message Nested { + google.api.HttpBody body = 500000000; + } + Nested nested = 100000000; + } + Nested nested = 1000000; + } + Nested nested = 1; +} \ No newline at end of file