Skip to content
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

Merged
merged 25 commits into from
Jul 13, 2018
Merged

grpc-json: handle google.api.HttpBody when building HTTP response #3793

merged 25 commits into from
Jul 13, 2018

Conversation

dio
Copy link
Member

@dio dio commented Jul 5, 2018

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:

  • Added notes on sending arbitrary content as HTTP response by using google.api.HttpBody.

Release Notes:

  • Added support for encoding google.api.HttpBody when building HTTP response.

Fixes #3205

Signed-off-by: Dhi Aurrahman dio@rockybars.com

dio added 5 commits July 5, 2018 13:39
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>
dio added 2 commits July 6, 2018 08:13
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Copy link
Member

@lizan lizan left a 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!

@@ -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
Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member

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_views.

Copy link
Member Author

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?

Copy link
Member Author

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()) {
Copy link
Member

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?

Copy link
Member Author

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 {
Copy link
Member

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.

Copy link
Member Author

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) {
Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -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>`_.
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

@dio dio changed the title grpc-json: handle google.api.HttpBody grpc-json: handle google.api.HttpBody when building HTTP response Jul 9, 2018
dio added 4 commits July 9, 2018 14:01
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;
Copy link
Member Author

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?

Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

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.

dio added 2 commits July 10, 2018 14:30
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Copy link
Member

@lizan lizan left a 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();
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, hasHttpBodyAsOutputType() && !server_streaming

Copy link
Member Author

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.
Copy link
Member

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.

dio added 2 commits July 11, 2018 09:57
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
@dio
Copy link
Member Author

dio commented Jul 11, 2018

Thanks, @lizan. I think I have addressed your comments. PTAL when you have time. Thanks again!

@lizan
Copy link
Member

lizan commented Jul 11, 2018

@dio can you fix the tests?

@dio
Copy link
Member Author

dio commented Jul 11, 2018

Sure. Sorry was not aware of it.

dio added 2 commits July 11, 2018 13:45
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
lizan
lizan previously approved these changes Jul 11, 2018
@lizan lizan requested a review from htuch July 11, 2018 22:25
Copy link
Member

@htuch htuch left a 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(
Copy link
Member

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.

Copy link
Member Author

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.
Copy link
Member

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);
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

@dio dio Jul 12, 2018

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);.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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)

Copy link
Member

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.

Copy link
Member Author

@dio dio Jul 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lizan added d93f57e.

Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
dio added 2 commits July 13, 2018 00:27
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));
Copy link
Member

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?

Copy link
Member Author

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>
htuch
htuch previously approved these changes Jul 12, 2018
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks.

@htuch
Copy link
Member

htuch commented Jul 12, 2018

@dio can you merge master to fix version_history.rst conflict?

Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
@dio
Copy link
Member Author

dio commented Jul 13, 2018

@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.
Copy link
Member

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.

Copy link
Member Author

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();
Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member Author

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. 🤞

Copy link
Member

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

dio added 2 commits July 13, 2018 17:13
Thanks @lizan!

Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Signed-off-by: Dhi Aurrahman <dio@rockybars.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants