Skip to content

Commit

Permalink
add error details to error json (grpc-ecosystem#515)
Browse files Browse the repository at this point in the history
Signed-off-by: Stephan Renatus <[email protected]>
  • Loading branch information
srenatus authored and achew22 committed Jan 11, 2018
1 parent e6ec145 commit d1dd61c
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 112 deletions.
59 changes: 59 additions & 0 deletions examples/clients/abe/a_bit_of_everything_service_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,65 @@ func (a ABitOfEverythingServiceApi) Echo3(value string) (*SubStringMessage, *API
return successPayload, localVarAPIResponse, err
}

/**
*
*
* @return *ProtobufEmpty
*/
func (a ABitOfEverythingServiceApi) ErrorWithDetails() (*ProtobufEmpty, *APIResponse, error) {

var localVarHttpMethod = strings.ToUpper("Get")
// create path and map variables
localVarPath := a.Configuration.BasePath + "/v2/example/errorwithdetails"

localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := make(map[string]string)
var localVarPostBody interface{}
var localVarFileName string
var localVarFileBytes []byte
// add default headers if any
for key := range a.Configuration.DefaultHeader {
localVarHeaderParams[key] = a.Configuration.DefaultHeader[key]
}

// to determine the Content-Type header
localVarHttpContentTypes := []string{ "application/json", "application/x-foo-mime", }

// set Content-Type header
localVarHttpContentType := a.Configuration.APIClient.SelectHeaderContentType(localVarHttpContentTypes)
if localVarHttpContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHttpContentType
}
// to determine the Accept header
localVarHttpHeaderAccepts := []string{
"application/json",
"application/x-foo-mime",
}

// set Accept header
localVarHttpHeaderAccept := a.Configuration.APIClient.SelectHeaderAccept(localVarHttpHeaderAccepts)
if localVarHttpHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHttpHeaderAccept
}
var successPayload = new(ProtobufEmpty)
localVarHttpResponse, err := a.Configuration.APIClient.CallAPI(localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes)

var localVarURL, _ = url.Parse(localVarPath)
localVarURL.RawQuery = localVarQueryParams.Encode()
var localVarAPIResponse = &APIResponse{Operation: "ErrorWithDetails", Method: localVarHttpMethod, RequestURL: localVarURL.String()}
if localVarHttpResponse != nil {
localVarAPIResponse.Response = localVarHttpResponse.RawResponse
localVarAPIResponse.Payload = localVarHttpResponse.Body()
}

if err != nil {
return successPayload, localVarAPIResponse, err
}
err = json.Unmarshal(localVarHttpResponse.Body(), &successPayload)
return successPayload, localVarAPIResponse, err
}

/**
*
*
Expand Down
245 changes: 140 additions & 105 deletions examples/examplepb/a_bit_of_everything.pb.go

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions examples/examplepb/a_bit_of_everything.pb.gw.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/examplepb/a_bit_of_everything.proto
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ service ABitOfEverythingService {
get: "/v2/example/timeout",
};
}
rpc ErrorWithDetails(google.protobuf.Empty) returns (google.protobuf.Empty) {
option (google.api.http) = {
get: "/v2/example/errorwithdetails",
};
}
rpc GetMessageWithBody(MessageWithBody) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v2/example/withbody/{id}",
Expand Down
16 changes: 16 additions & 0 deletions examples/examplepb/a_bit_of_everything.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,22 @@
}
}
},
"/v2/example/errorwithdetails": {
"get": {
"operationId": "ErrorWithDetails",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/protobufEmpty"
}
}
},
"tags": [
"ABitOfEverythingService"
]
}
},
"/v2/example/timeout": {
"get": {
"operationId": "Timeout",
Expand Down
60 changes: 57 additions & 3 deletions examples/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import (
)

type errorBody struct {
Error string `json:"error"`
Code int `json:"code"`
Error string `json:"error"`
Code int `json:"code"`
Details []interface{} `json:"details"`
}

func TestEcho(t *testing.T) {
Expand Down Expand Up @@ -460,7 +461,7 @@ func testABELookupNotFound(t *testing.T, port int) {

var msg errorBody
if err := json.Unmarshal(buf, &msg); err != nil {
t.Errorf("jsonpb.UnmarshalString(%s, &msg) failed with %v; want success", buf, err)
t.Errorf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err)
return
}

Expand Down Expand Up @@ -723,6 +724,59 @@ func TestTimeout(t *testing.T) {
}
}

func TestErrorWithDetails(t *testing.T) {
url := "http://localhost:8080/v2/example/errorwithdetails"
resp, err := http.Get(url)
if err != nil {
t.Errorf("http.Get(%q) failed with %v; want success", url, err)
return
}
defer resp.Body.Close()

buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("iotuil.ReadAll(resp.Body) failed with %v; want success", err)
}

if got, want := resp.StatusCode, http.StatusInternalServerError; got != want {
t.Errorf("resp.StatusCode = %d; want %d", got, want)
}

var msg errorBody
if err := json.Unmarshal(buf, &msg); err != nil {
t.Fatalf("json.Unmarshal(%s, &msg) failed with %v; want success", buf, err)
}

if got, want := msg.Code, int(codes.Unknown); got != want {
t.Errorf("msg.Code = %d; want %d", got, want)
}
if got, want := msg.Error, "with details"; got != want {
t.Errorf("msg.Error = %s; want %s", got, want)
}
if got, want := len(msg.Details), 1; got != want {
t.Fatalf("len(msg.Details) = %q; want %q", got, want)
}

details, ok := msg.Details[0].(map[string]interface{})
if got, want := ok, true; got != want {
t.Fatalf("msg.Details[0] got type: %T, want %T", msg.Details[0], map[string]interface{}{})
}
if got, want := details["detail"], "error debug details"; got != want {
t.Errorf("msg.Details[\"detail\"] = %q; want %q", got, want)
}
entries, ok := details["stack_entries"].([]interface{})
if got, want := ok, true; got != want {
t.Fatalf("msg.Details[0][\"stack_entries\"] got type: %T, want %T", entries, []string{})
}
entry, ok := entries[0].(string)
if got, want := ok, true; got != want {
t.Fatalf("msg.Details[0][\"stack_entries\"][0] got type: %T, want %T", entry, "")
}
if got, want := entries[0], "foo:1"; got != want {
t.Errorf("msg.Details[\"stack_entries\"][0] = %q; want %q", got, want)
}
}

func TestUnknownPath(t *testing.T) {
url := "http://localhost:8080"
resp, err := http.Post(url, "application/json", strings.NewReader("{}"))
Expand Down
17 changes: 16 additions & 1 deletion examples/server/a_bit_of_everything.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import (
"sync"

"github.com/golang/glog"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/duration"
"github.com/golang/protobuf/ptypes/empty"
examples "github.com/grpc-ecosystem/grpc-gateway/examples/examplepb"
sub "github.com/grpc-ecosystem/grpc-gateway/examples/sub"
sub2 "github.com/grpc-ecosystem/grpc-gateway/examples/sub2"
"github.com/rogpeppe/fastuuid"
"golang.org/x/net/context"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"

)

// Implements of ABitOfEverythingServiceServer
Expand Down Expand Up @@ -248,6 +249,20 @@ func (s *_ABitOfEverythingServer) Timeout(ctx context.Context, msg *empty.Empty)
}
}

func (s *_ABitOfEverythingServer) ErrorWithDetails(ctx context.Context, msg *empty.Empty) (*empty.Empty, error) {
stat, err := status.New(codes.Unknown, "with details").
WithDetails(proto.Message(
&errdetails.DebugInfo{
StackEntries: []string{"foo:1"},
Detail: "error debug details",
},
))
if err != nil {
return nil, status.Errorf(codes.Internal, "unexpected error adding details: %s", err)
}
return nil, stat.Err()
}

func (s *_ABitOfEverythingServer) GetMessageWithBody(ctx context.Context, msg *examples.MessageWithBody) (*empty.Empty, error) {
return &empty.Empty{}, nil
}
13 changes: 10 additions & 3 deletions runtime/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ var (
)

type errorBody struct {
Error string `protobuf:"bytes,1,name=error" json:"error"`
Code int32 `protobuf:"varint,2,name=code" json:"code"`
Error string `protobuf:"bytes,1,name=error" json:"error"`
Code int32 `protobuf:"varint,2,name=code" json:"code"`
Details []proto.Message `protobuf:"bytes,3,name=details" json:"details"`
}

//Make this also conform to proto.Message for builtin JSONPb Marshaler
// Make this also conform to proto.Message for builtin JSONPb Marshaler
func (e *errorBody) Reset() { *e = errorBody{} }
func (e *errorBody) String() string { return proto.CompactTextString(e) }
func (*errorBody) ProtoMessage() {}
Expand All @@ -94,6 +95,12 @@ func DefaultHTTPError(ctx context.Context, mux *ServeMux, marshaler Marshaler, w
Code: int32(s.Code()),
}

for _, detail := range s.Details() {
if det, ok := detail.(proto.Message); ok {
body.Details = append(body.Details, det)
}
}

buf, merr := marshaler.Marshal(body)
if merr != nil {
grpclog.Printf("Failed to marshal error message %q: %v", body, merr)
Expand Down

0 comments on commit d1dd61c

Please sign in to comment.