-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
grpc-json: handle google.api.HttpBody when building HTTP response #3793
Changes from 7 commits
ce46d96
aa8e4cf
ac4455b
fcd37be
da90483
59cf968
6dcd9a6
5aa346a
9f4739b
95778e8
e76880e
eff3c80
3aa68fb
4123018
085e967
d5c4ba9
784d789
c2dc9e8
e9d9b1e
c0d7c87
b45854a
d93f57e
b9a5e43
40d9b3f
9403723
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,14 +21,62 @@ def api_dependencies(): | |
load("@com_google_protobuf//:protobuf.bzl", "cc_proto_library", "py_proto_library") | ||
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") | ||
|
||
filegroup( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we keep doing this for every Google API proto we bring in, it will be quite verbose. I think it's outside the scope of this PR, but we should consider writing a Skylark macro to shrink the boilerplate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a TODO for this. I hope that's OK for now. |
||
name = "api_httpbody_protos_src", | ||
srcs = [ | ||
"google/api/httpbody.proto", | ||
], | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
proto_library( | ||
name = "api_httpbody_protos_proto", | ||
srcs = [":api_httpbody_protos_src"], | ||
deps = ["@com_google_protobuf//:descriptor_proto"], | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
cc_proto_library( | ||
name = "api_httpbody_protos", | ||
srcs = [ | ||
"google/api/httpbody.proto", | ||
], | ||
default_runtime = "@com_google_protobuf//:protobuf", | ||
protoc = "@com_google_protobuf//:protoc", | ||
deps = ["@com_google_protobuf//:cc_wkt_protos"], | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
py_proto_library( | ||
name = "api_httpbody_protos_py", | ||
srcs = [ | ||
"google/api/httpbody.proto", | ||
], | ||
include = ".", | ||
default_runtime = "@com_google_protobuf//:protobuf_python", | ||
protoc = "@com_google_protobuf//:protoc", | ||
visibility = ["//visibility:public"], | ||
deps = ["@com_google_protobuf//:protobuf_python"], | ||
) | ||
|
||
go_proto_library( | ||
name = "api_httpbody_go_proto", | ||
importpath = "google.golang.org/genproto/googleapis/api/httpbody", | ||
proto = ":api_httpbody_protos_proto", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
":descriptor_go_proto", | ||
], | ||
) | ||
|
||
filegroup( | ||
name = "http_api_protos_src", | ||
srcs = [ | ||
"google/api/annotations.proto", | ||
"google/api/http.proto", | ||
], | ||
visibility = ["//visibility:public"], | ||
) | ||
) | ||
|
||
go_proto_library( | ||
name = "descriptor_go_proto", | ||
|
@@ -93,6 +141,7 @@ proto_library( | |
deps = ["@com_google_protobuf//:any_proto"], | ||
visibility = ["//visibility:public"], | ||
) | ||
|
||
cc_proto_library( | ||
name = "rpc_status_protos", | ||
srcs = ["google/rpc/status.proto"], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,3 +62,14 @@ match the incoming request path, set `match_incoming_request_route` to true. | |
}; | ||
} | ||
} | ||
|
||
Sending arbitrary content | ||
------------------------- | ||
|
||
By default, when transcoding occurs, gRPC-JSON encodes the message output of a gRPC service method into | ||
JSON and sets the response `Content-Type` header to `application/json`. To send abritrary content, a gRPC | ||
service method can use | ||
`google.api.HttpBody <https://github.com/googleapis/googleapis/blob/master/google/api/httpbody.proto>`_ | ||
as its output message type. The implementation needs to set | ||
`content_type <https://github.com/googleapis/googleapis/blob/master/google/api/httpbody.proto#L68>`_ | ||
and `data <https://github.com/googleapis/googleapis/blob/master/google/api/httpbody.proto#L71>`_ accordingly. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe point out that content-type is derived from the proto in this case. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ Version history | |
to filter based on the presence of Envoy response flags. | ||
* admin: added :http:get:`/hystrix_event_stream` as an endpoint for monitoring envoy's statistics | ||
through `Hystrix dashboard <https://github.com/Netflix-Skunkworks/hystrix-dashboard/wiki>`_. | ||
* grpc-json: added support for encoding `google.api.HttpBody <https://github.com/googleapis/googleapis/blob/master/google/api/httpbody.proto>`_. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's clarify this is for response only, and update PR title. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
* health check: added support for :ref:`custom health check <envoy_api_field_core.HealthCheck.custom_health_check>`. | ||
* health_check: added support for :ref:`health check event logging <arch_overview_health_check_logging>`. | ||
* http: response filters not applied to early error paths such as http_parser generated 400s. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,16 +4,19 @@ | |
#include "envoy/http/filter.h" | ||
|
||
#include "common/common/assert.h" | ||
#include "common/common/base64.h" | ||
#include "common/common/enum_to_int.h" | ||
#include "common/common/utility.h" | ||
#include "common/filesystem/filesystem_impl.h" | ||
#include "common/grpc/common.h" | ||
#include "common/http/headers.h" | ||
#include "common/http/utility.h" | ||
#include "common/json/json_loader.h" | ||
#include "common/protobuf/protobuf.h" | ||
|
||
#include "google/api/annotations.pb.h" | ||
#include "google/api/http.pb.h" | ||
#include "google/api/httpbody.pb.h" | ||
#include "grpc_transcoding/json_request_translator.h" | ||
#include "grpc_transcoding/path_matcher_utility.h" | ||
#include "grpc_transcoding/response_to_json_translator.h" | ||
|
@@ -349,6 +352,10 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, | |
readToBuffer(*transcoder_->ResponseOutput(), data); | ||
|
||
if (!method_->server_streaming() && !end_stream) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. support or add a TODO for streaming case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
if (hasHttpBodyAsOutputType()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we check this earlier (preferred in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
// Set content-type and data from HttpBody. | ||
buildResponseFromHttpBodyOutput(*response_headers_, data); | ||
} | ||
// Buffer until the response is complete. | ||
return Http::FilterDataStatus::StopIterationAndBuffer; | ||
} | ||
|
@@ -415,6 +422,32 @@ bool JsonTranscoderFilter::readToBuffer(Protobuf::io::ZeroCopyInputStream& strea | |
return false; | ||
} | ||
|
||
void JsonTranscoderFilter::buildResponseFromHttpBodyOutput(Http::HeaderMap& response_headers, | ||
Buffer::Instance& data) { | ||
try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of parsing JSON here, it would be better to parse protobuf directly using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, this is new to me. Thanks for pointing it out. |
||
const Json::ObjectSharedPtr http_body = Json::Factory::loadFromString(data.toString()); | ||
const std::string decoded_body = Base64::decode(http_body->getString("data")); | ||
|
||
data.drain(data.length()); | ||
data.add(decoded_body); | ||
|
||
response_headers.insertContentType().value(http_body->getString("contentType")); | ||
response_headers.insertContentLength().value(decoded_body.size()); | ||
} catch (const Json::Exception& e) { | ||
ENVOY_LOG(debug, "Failed to parse output of '{}'. e.what(): {}", method_->full_name(), | ||
e.what()); | ||
} | ||
} | ||
|
||
bool JsonTranscoderFilter::hasHttpBodyAsOutputType() { | ||
absl::string_view output_type = method_->output_type()->full_name(); | ||
absl::string_view http_body_type = google::api::HttpBody::descriptor()->full_name(); | ||
if (output_type.length() != http_body_type.length()) { | ||
return false; | ||
} | ||
return StringUtil::startsWith(output_type.data(), http_body_type.data(), true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why startsWith but not equal here? You should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have the length comparison previously. And since the comparison needs to be case-sensitive? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will use the equal operator then. |
||
} | ||
|
||
} // namespace GrpcJsonTranscoder | ||
} // namespace HttpFilters | ||
} // namespace Extensions | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -550,6 +550,47 @@ TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryNotGrpcResponse) { | |
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.encodeData(request_data, true)); | ||
} | ||
|
||
TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryWithHttpBodyAsOutput) { | ||
Http::TestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/index"}}; | ||
|
||
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("/index", request_headers.get_("x-envoy-original-path")); | ||
EXPECT_EQ("/bookstore.Bookstore/GetIndex", request_headers.get_(":path")); | ||
EXPECT_EQ("trailers", request_headers.get_("te")); | ||
|
||
Http::TestHeaderMapImpl continue_headers{{":status", "000"}}; | ||
EXPECT_EQ(Http::FilterHeadersStatus::Continue, | ||
filter_.encode100ContinueHeaders(continue_headers)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the 100 continue stuff here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed in d93f57e. |
||
|
||
Http::TestHeaderMapImpl response_headers{{"content-type", "application/grpc"}, | ||
{":status", "200"}}; | ||
|
||
EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, | ||
filter_.encodeHeaders(response_headers, false)); | ||
EXPECT_EQ("application/json", response_headers.get_("content-type")); | ||
|
||
google::api::HttpBody response; | ||
response.set_content_type("text/html"); | ||
response.set_data("<h1>Hello, world!</h1>"); | ||
|
||
auto response_data = Grpc::Common::serializeBody(response); | ||
|
||
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, | ||
filter_.encodeData(*response_data, false)); | ||
|
||
std::string response_html = response_data->toString(); | ||
|
||
EXPECT_EQ("text/html", response_headers.get_("content-type")); | ||
EXPECT_EQ("<h1>Hello, world!</h1>", response_html); | ||
|
||
Http::TestHeaderMapImpl response_trailers{{"grpc-status", "0"}, {"grpc-message", ""}}; | ||
|
||
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(response_trailers)); | ||
} | ||
|
||
struct GrpcJsonTranscoderFilterPrintTestParam { | ||
std::string config_json_; | ||
std::string expected_response_; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this similar issue to bazelbuild/bazel#5163 ?
Though I don't think we should add this here to every
api_proto_library
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I will remove the comment.