-
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
Conversation
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
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.
Overall looks great, thanks for doing this @dio!
api/bazel/api_build_system.bzl
Outdated
@@ -125,6 +127,11 @@ def api_proto_library(name, visibility = ["//visibility:private"], srcs = [], de | |||
deps = [_LibrarySuffix(d, _CC_SUFFIX) for d in deps], | |||
external_deps = [ | |||
"@com_google_protobuf//:cc_wkt_protos", | |||
# TODO(dio): Running `bazel build @envoy_api//envoy/...` on mac |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
why startsWith but not equal here? You should use absl::StartsWith
for string_view
s.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Will use the equal operator then.
@@ -349,6 +352,10 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, | |||
readToBuffer(*transcoder_->ResponseOutput(), data); | |||
|
|||
if (!method_->server_streaming() && !end_stream) { | |||
if (hasHttpBodyAsOutputType()) { |
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.
Can we check this earlier (preferred in decodeData
right after creating transcoder_
) and store in a member variable?
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.
👍
@@ -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 comment
The 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 Grpc::Frame
and Grpc::Decoder
. It is cleaner and more efficient, hardcoded "contentType"
won't work when preserve_field_names
is set to true.
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.
Cool, this is new to me. Thanks for pointing it out.
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
👍
docs/root/intro/version_history.rst
Outdated
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
OK
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
data.add(body); | ||
response_headers.insertContentType().value(http_body.content_type()); | ||
response_headers.insertContentLength().value(body.length()); | ||
return true; |
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.
I'm not really sure about this one. Should we handle more than one frame?
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.
Since you left a TODO to support streaming, so it is ok only process one frame here.
data.add(body); | ||
response_headers.insertContentType().value(http_body.content_type()); | ||
response_headers.insertContentLength().value(body.length()); | ||
return true; |
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.
Since you left a TODO to support streaming, so it is ok only process one frame here.
return true; | ||
} | ||
} | ||
return false; |
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.
In the case of one frame with length 0, what is the point to let this return false and let the default handler transcode to {}? It is valid that a response HttpBody have empty content type and data.
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
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.
two nits, thank you!
@@ -222,6 +223,7 @@ Http::FilterHeadersStatus JsonTranscoderFilter::decodeHeaders(Http::HeaderMap& h | |||
// just pass-through the request to upstream. | |||
return Http::FilterHeadersStatus::Continue; | |||
} | |||
has_http_body_output_ = hasHttpBodyAsOutputType(); |
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.
Since the behavior below depends on non-streaming handling, (i.e. buffer response until trailer), lets only set this when server_streaming is false.
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.
Hi @lizan sorry, I need to clarify this. Do you want to make sure that we only set has_http_body_output_
to true when server_streaming
is false and hasHttpBodyAsOutputType()
is true?
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.
yes, hasHttpBodyAsOutputType() && !server_streaming
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.
Ah OK, done. Thanks.
@@ -352,6 +359,7 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, | |||
// Buffer until the response is complete. | |||
return Http::FilterDataStatus::StopIterationAndBuffer; | |||
} | |||
// TODO(dio): Add support for streaming case. |
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.
move this todo to L226 or L345.
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Thanks, @lizan. I think I have addressed your comments. PTAL when you have time. Thanks again! |
@dio can you fix the tests? |
Sure. Sorry was not aware of it. |
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
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.
Looks good, question about the encodeData
assumptions.
@@ -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 comment
The 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 comment
The 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.
`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 comment
The 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.
@@ -340,6 +342,12 @@ Http::FilterDataStatus JsonTranscoderFilter::encodeData(Buffer::Instance& data, | |||
return Http::FilterDataStatus::Continue; | |||
} | |||
|
|||
// TODO(dio): Add support for streaming case. | |||
if (has_http_body_output_) { | |||
buildResponseFromHttpBodyOutput(*response_headers_, data); |
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.
Are we assuming here that the entire HTTP response proto is available in a single encodeData
invocation? I don't think that's true, even for the non-streaming case.
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.
Thanks, @htuch. It makes sense, it seems it needs to buffer the data. Will try to explore it.
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.
I think if we don't have enough data to decode for a single frame, we buffer it in here: decoder_.decode(data, frames);
.
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.
Yeah, fair enough. How do we actually continue processing when we're done though? Looks like we're just always returning Http::FilterDataStatus::StopIterationAndBuffer?
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.
@htuch I think @dio already does this correctly, decoder_.decode(data, frames);
buffers the data, and it returns Http::FilterDataStatus::StopIterationAndBuffer
to stop the iteration, because the filter need content-type from grpc-frame, and potentially grpc-status in trailer. The transcoder will stop filter iteration until it can send full response (i.e. until encodeTrailer) for unary cases.
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.
@dio can you add some test case to cover this? (one grpc frame split in two encodeData call)
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.
@lizan got it, makes sense, thanks.
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.
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
|
||
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in d93f57e.
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
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.
Great, thanks.
@dio can you merge master to fix |
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
@htuch merged with master. |
auto response_data = Grpc::Common::serializeBody(response); | ||
EXPECT_EQ(40, response_data->length()); | ||
|
||
// The response data buffer is splitted into two parts. |
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.
Buffer::OwnedImpl part1;
part1.move(response_data, 20);
will do what L615-L625 does.
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.
Thank you for this. Updated in 40d9b3f.
part2.add(std::string(out.begin(), out.end())); | ||
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, filter_.encodeData(part2, false)); | ||
|
||
const std::string response_html = part2.toString(); |
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.
It is not intuitive why part2 will be response_html, I would just set response_html directly from the string "<h1>Hello, world!</h1>"
and use it with set_data and expectation below.
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.
My proposed change is in 40d9b3f.
I would just set response_html directly from the string
"<h1>Hello, world!</h1>"
and use it with set_data and expectation below.
I'm not sure I get this. Would you mind to elaborate? Thanks!
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.
Hopefully, the change in 9403723 is better. 🤞
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.
Ah yep that is better 👍 LGTM
Thanks @lizan! Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Description:
This patch prepares HTTP response when an upstream gRPC service method
uses
google.api.HttpBody
proto as its message output type.Risk Level: Low
Testing: Unit
Docs Changes:
google.api.HttpBody
.Release Notes:
google.api.HttpBody
when building HTTP response.Fixes #3205
Signed-off-by: Dhi Aurrahman dio@rockybars.com