Skip to content

Commit

Permalink
New proposal: support for the google.api.HttpBody (#904)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wimspaargaren authored and johanbrandhorst committed Mar 11, 2019
1 parent 6523154 commit 7aca14d
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 8 deletions.
43 changes: 43 additions & 0 deletions docs/_docs/httpbody.md
Original file line number Diff line number Diff line change
@@ -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
}

```
17 changes: 13 additions & 4 deletions runtime/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(),
Expand Down
11 changes: 10 additions & 1 deletion runtime/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions runtime/marshal_httpbodyproto.go
Original file line number Diff line number Diff line change
@@ -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)
}
49 changes: 49 additions & 0 deletions runtime/marshal_httpbodyproto_test.go
Original file line number Diff line number Diff line change
@@ -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)

}
}
15 changes: 12 additions & 3 deletions runtime/proto_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions third_party/googleapis/README.grpc-gateway
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Imported Files

- google/api/annotations.proto
- google/api/http.proto
- google/api/httpbody.proto


Generated Files
Expand Down
78 changes: 78 additions & 0 deletions third_party/googleapis/google/api/httpbody.proto
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 7aca14d

Please sign in to comment.