From 7aca14d0a16247151f6e423f4cbfbd75a1bd4776 Mon Sep 17 00:00:00 2001 From: Wim Spaargaren Date: Mon, 11 Mar 2019 17:25:07 +0100 Subject: [PATCH] New proposal: support for the google.api.HttpBody (#904) * Implementation of HTTPBody marshaler * Rework HTTPBodyMarshaler fallback marshaler * Added tests. * Fix gomod * Fixed tests and added two new ones. * Added marshal backwards compatibility * Last line modsum was removed. * Embedded Marshaler & fixed import order * Removed inherited methods. * Comment improvements * Added small readme. * Doc enhancements * Identation fix * What's wrong with identation --- docs/_docs/httpbody.md | 43 ++++++++++ runtime/errors.go | 17 +++- runtime/handler.go | 11 ++- runtime/marshal_httpbodyproto.go | 43 ++++++++++ runtime/marshal_httpbodyproto_test.go | 49 ++++++++++++ runtime/proto_errors.go | 15 +++- third_party/googleapis/README.grpc-gateway | 1 + .../googleapis/google/api/httpbody.proto | 78 +++++++++++++++++++ 8 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 docs/_docs/httpbody.md create mode 100644 runtime/marshal_httpbodyproto.go create mode 100644 runtime/marshal_httpbodyproto_test.go create mode 100644 third_party/googleapis/google/api/httpbody.proto diff --git a/docs/_docs/httpbody.md b/docs/_docs/httpbody.md new file mode 100644 index 00000000000..f95148ab336 --- /dev/null +++ b/docs/_docs/httpbody.md @@ -0,0 +1,43 @@ +--- +category: documentation +--- + +# HttpBody message Feature +The [HTTP Body](https://github.com/googleapis/googleapis/blob/master/google/api/httpbody.proto) messages allows a response message to be specified with custom data content and a custom content type header. The values included in the HTTPBody response will be used verbatim in the returned message from the gateway. Make sure you format your response carefully! + +## Example Usage +1. Create a mux with the HTTP Body Marshaler as option. + +```golang + mux := runtime.NewServeMux(runtime.SetHTTPBodyMarshaler) +``` +2. Define your service in gRPC with an httpbody response message + +```golang +import "google/api/httpbody.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/empty.proto"; + +service HttpBodyExampleService { + + rpc HelloWorld(google.protobuf.Empty) returns (google.api.HttpBody) { + option (google.api.http) = { + get: "/helloworld" + }; + } + +} +``` +3. Generate gRPC and reverse-proxy stubs and implement your service. + +## Example service implementation + +```golang +func (*HttpBodyExampleService) Helloworld(ctx context.Context, in *empty.Empty) (*httpbody.HttpBody, error) { + return &httpbody.HttpBody{ + ContentType: "text/html", + Data: []byte("Hello World"), + }, nil +} + +``` \ No newline at end of file diff --git a/runtime/errors.go b/runtime/errors.go index b39f55ea824..41d54ef916a 100644 --- a/runtime/errors.go +++ b/runtime/errors.go @@ -65,7 +65,7 @@ var ( ) type errorBody struct { - Error string `protobuf:"bytes,1,name=error" json:"error"` + Error string `protobuf:"bytes,1,name=error" json:"error"` // This is to make the error more compatible with users that expect errors to be Status objects: // https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto // It should be the exact same message as the Error field. @@ -88,14 +88,23 @@ func (*errorBody) ProtoMessage() {} func DefaultHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w http.ResponseWriter, _ *http.Request, err error) { const fallback = `{"error": "failed to marshal error message"}` - w.Header().Del("Trailer") - w.Header().Set("Content-Type", marshaler.ContentType()) - s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } + w.Header().Del("Trailer") + + contentType := marshaler.ContentType() + // Check marshaler on run time in order to keep backwards compatability + // An interface param needs to be added to the ContentType() function on + // the Marshal interface to be able to remove this check + if httpBodyMarshaler, ok := marshaler.(*HTTPBodyMarshaler); ok { + pb := s.Proto() + contentType = httpBodyMarshaler.ContentTypeFromMessage(pb) + } + w.Header().Set("Content-Type", contentType) + body := &errorBody{ Error: s.Message(), Message: s.Message(), diff --git a/runtime/handler.go b/runtime/handler.go index 58ba686b81b..1fc63f7f58b 100644 --- a/runtime/handler.go +++ b/runtime/handler.go @@ -121,7 +121,16 @@ func ForwardResponseMessage(ctx context.Context, mux *ServeMux, marshaler Marsha handleForwardResponseServerMetadata(w, mux, md) handleForwardResponseTrailerHeader(w, md) - w.Header().Set("Content-Type", marshaler.ContentType()) + + contentType := marshaler.ContentType() + // Check marshaler on run time in order to keep backwards compatability + // An interface param needs to be added to the ContentType() function on + // the Marshal interface to be able to remove this check + if httpBodyMarshaler, ok := marshaler.(*HTTPBodyMarshaler); ok { + contentType = httpBodyMarshaler.ContentTypeFromMessage(resp) + } + w.Header().Set("Content-Type", contentType) + if err := handleForwardResponseOptions(ctx, w, resp, opts); err != nil { HTTPError(ctx, mux, marshaler, w, req, err) return diff --git a/runtime/marshal_httpbodyproto.go b/runtime/marshal_httpbodyproto.go new file mode 100644 index 00000000000..f55285b5d6c --- /dev/null +++ b/runtime/marshal_httpbodyproto.go @@ -0,0 +1,43 @@ +package runtime + +import ( + "google.golang.org/genproto/googleapis/api/httpbody" +) + +// SetHTTPBodyMarshaler overwrite the default marshaler with the HTTPBodyMarshaler +func SetHTTPBodyMarshaler(serveMux *ServeMux) { + serveMux.marshalers.mimeMap[MIMEWildcard] = &HTTPBodyMarshaler{ + Marshaler: &JSONPb{OrigName: true}, + } +} + +// HTTPBodyMarshaler is a Marshaler which supports marshaling of a +// google.api.HttpBody message as the full response body if it is +// the actual message used as the response. If not, then this will +// simply fallback to the Marshaler specified as its default Marshaler. +type HTTPBodyMarshaler struct { + Marshaler +} + +// ContentType implementation to keep backwards compatability with marshal interface +func (h *HTTPBodyMarshaler) ContentType() string { + return h.ContentTypeFromMessage(nil) +} + +// ContentTypeFromMessage in case v is a google.api.HttpBody message it returns +// its specified content type otherwise fall back to the default Marshaler. +func (h *HTTPBodyMarshaler) ContentTypeFromMessage(v interface{}) string { + if httpBody, ok := v.(*httpbody.HttpBody); ok { + return httpBody.GetContentType() + } + return h.Marshaler.ContentType() +} + +// Marshal marshals "v" by returning the body bytes if v is a +// google.api.HttpBody message, otherwise it falls back to the default Marshaler. +func (h *HTTPBodyMarshaler) Marshal(v interface{}) ([]byte, error) { + if httpBody, ok := v.(*httpbody.HttpBody); ok { + return httpBody.Data, nil + } + return h.Marshaler.Marshal(v) +} diff --git a/runtime/marshal_httpbodyproto_test.go b/runtime/marshal_httpbodyproto_test.go new file mode 100644 index 00000000000..1f07022770a --- /dev/null +++ b/runtime/marshal_httpbodyproto_test.go @@ -0,0 +1,49 @@ +package runtime_test + +import ( + "bytes" + "testing" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "google.golang.org/genproto/googleapis/api/httpbody" +) + +func TestHTTPBodyContentType(t *testing.T) { + m := runtime.HTTPBodyMarshaler{ + &runtime.JSONPb{ + OrigName: true, + }, + } + expected := "CustomContentType" + message := &httpbody.HttpBody{ + ContentType: expected, + } + res := m.ContentType() + if res != "application/json" { + t.Errorf("content type not equal (%q, %q)", res, expected) + } + res = m.ContentTypeFromMessage(message) + if res != expected { + t.Errorf("content type not equal (%q, %q)", res, expected) + } +} + +func TestHTTPBodyMarshal(t *testing.T) { + m := runtime.HTTPBodyMarshaler{ + &runtime.JSONPb{ + OrigName: true, + }, + } + expected := []byte("Some test") + message := &httpbody.HttpBody{ + Data: expected, + } + res, err := m.Marshal(message) + if err != nil { + t.Errorf("m.Marshal(%#v) failed with %v; want success", message, err) + } + if !bytes.Equal(res, expected) { + t.Errorf("Marshalled data not equal (%q, %q)", res, expected) + + } +} diff --git a/runtime/proto_errors.go b/runtime/proto_errors.go index 43fafca7c20..b7fa32e45db 100644 --- a/runtime/proto_errors.go +++ b/runtime/proto_errors.go @@ -26,14 +26,23 @@ func DefaultHTTPProtoErrorHandler(ctx context.Context, mux *ServeMux, marshaler // return Internal when Marshal failed const fallback = `{"code": 13, "message": "failed to marshal error message"}` - w.Header().Del("Trailer") - w.Header().Set("Content-Type", marshaler.ContentType()) - s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } + w.Header().Del("Trailer") + + contentType := marshaler.ContentType() + // Check marshaler on run time in order to keep backwards compatability + // An interface param needs to be added to the ContentType() function on + // the Marshal interface to be able to remove this check + if httpBodyMarshaler, ok := marshaler.(*HTTPBodyMarshaler); ok { + pb := s.Proto() + contentType = httpBodyMarshaler.ContentTypeFromMessage(pb) + } + w.Header().Set("Content-Type", contentType) + buf, merr := marshaler.Marshal(s.Proto()) if merr != nil { grpclog.Infof("Failed to marshal error message %q: %v", s.Proto(), merr) diff --git a/third_party/googleapis/README.grpc-gateway b/third_party/googleapis/README.grpc-gateway index 5c77aece630..b7d1bea36cd 100644 --- a/third_party/googleapis/README.grpc-gateway +++ b/third_party/googleapis/README.grpc-gateway @@ -12,6 +12,7 @@ Imported Files - google/api/annotations.proto - google/api/http.proto +- google/api/httpbody.proto Generated Files diff --git a/third_party/googleapis/google/api/httpbody.proto b/third_party/googleapis/google/api/httpbody.proto new file mode 100644 index 00000000000..4428515c120 --- /dev/null +++ b/third_party/googleapis/google/api/httpbody.proto @@ -0,0 +1,78 @@ +// Copyright 2018 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; +option java_multiple_files = true; +option java_outer_classname = "HttpBodyProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Message that represents an arbitrary HTTP body. It should only be used for +// payload formats that can't be represented as JSON, such as raw binary or +// an HTML page. +// +// +// This message can be used both in streaming and non-streaming API methods in +// the request as well as the response. +// +// It can be used as a top-level request field, which is convenient if one +// wants to extract parameters from either the URL or HTTP template into the +// request fields and also want access to the raw HTTP body. +// +// Example: +// +// message GetResourceRequest { +// // A unique request id. +// string request_id = 1; +// +// // The raw HTTP body is bound to this field. +// google.api.HttpBody http_body = 2; +// } +// +// service ResourceService { +// rpc GetResource(GetResourceRequest) returns (google.api.HttpBody); +// rpc UpdateResource(google.api.HttpBody) returns +// (google.protobuf.Empty); +// } +// +// Example with streaming methods: +// +// service CaldavService { +// rpc GetCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// rpc UpdateCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// } +// +// Use of this type only changes how the request and response bodies are +// handled, all other features will continue to work unchanged. +message HttpBody { + // The HTTP Content-Type header value specifying the content type of the body. + string content_type = 1; + + // The HTTP request/response body as raw binary. + bytes data = 2; + + // Application specific response metadata. Must be set in the first response + // for streaming APIs. + repeated google.protobuf.Any extensions = 3; +} \ No newline at end of file