From 64387558b6f9e8c116aca9204da97406f866dcfe Mon Sep 17 00:00:00 2001 From: Caleb Horst Date: Wed, 8 May 2019 10:42:02 -0700 Subject: [PATCH] Err fields (#165) * Enable extra fields in primary error returns with new function * Also add automatic code/status field setting option * Changes to readme and additional test cases * Cleanup code/status issues --- gateway/README.md | 100 ++++++++++++++++------------ gateway/errors.go | 79 ++++++++++++++++++++-- gateway/errors_test.go | 49 ++++++++++++++ gateway/response.go | 29 ++++++-- gateway/response_test.go | 140 +++++++++++++++++++++++++++++++++++++-- gateway/status.go | 19 +++--- gateway/status_test.go | 10 ++- 7 files changed, 354 insertions(+), 72 deletions(-) diff --git a/gateway/README.md b/gateway/README.md index e806e041..d24b825e 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -169,35 +169,69 @@ func (s *myService) MyMethod(req *MyRequest) (*MyResponse, error) { ### Response Format Unless another format is specified in the request `Accept` header that the service supports, services render resources in responses in JSON format by default. -Services must embed their response in a Success JSON structure. +By default for a successful RPC call only the proto response is rendered as JSON, however for a failed call a special format is used, and by calling special methods the response can include additional metadata. -The Success JSON structure provides a uniform structure for expressing normal responses using a structure similar to the Error JSON structure used to render errors. The structure provides an enumerated set of codes and associated HTTP statuses (see Errors below) along with a message. +The `WithSuccess(ctx context.Context, msg MessageWithFields)` function allows you to add a `success` block to the returned JSON. +By default this block only contains a message field, however arbitrary key-value pairs can also be included. +This is included at top level, alongside the assumed `result` or `results` field. -The Success JSON structure has the following format. The results tag is optional and appears when the response contains one or more resources. +Ex. ```json { "success": { - "status": , - "code": , + "foo": "bar", + "baz": 1, "message": }, "results": } ``` -The `results` content follows the [Google model](https://cloud.google.com/apis/design/standard_methods): an object is returned for Get, Create and Update operations and list of objects for List operation. +The `WithError(ctx context.Context, err error)` function allows you to add an extra `error` to the `errors` list in the returned JSON. +The `NewWithFields(message string, kvpairs ...interface{})` function can be used to create this error, which then includes additional fields in the error, otherwise only the error message will be included. +This is included at top level, alongside the assumed `result` or `results` field if the call succeeded despite the error, or alone otherwise. + +Ex. +```json +{ + "errors": [ + { + "foo": "bar", + "baz": 1, + "message": + } + ], + "results": +} +``` + +To return an error with fields and fail the RPC, return an error from `NewResponseError(ctx context.Context, msg string, kvpairs ...interface{})` or `NewResponseErrorWithCode(ctx context.Context, c codes.Code, msg string, kvpairs ...interface{})` to also set the return code. + +The function `IncludeStatusDetails(withDetails bool)` allows you to include the `success` block with fields `code` and `status` automatically for all responses, +and the first of the `errors` in failed responses will also include the fields. +Note that this choice affects all responses that pass through `gateway.ForwardResponseMessage`. -To allow compatibility with existing systems, the results tag name can be changed to a service-defined tag. In this way the success data becomes just a tag added to an existing structure. +Ex: +```json +{ + "success": { + "status": , + "code": , + "message": + }, + "results": +} +``` -#### Example Success Responses +#### Example Success Responses With IncludeStatusDetails(true) Response with no results ```json { "success": { - "status": 201, + "status": "CREATED", "message": "Account provisioned", - "code": "CREATED" + "code": 201 } } ``` @@ -206,9 +240,9 @@ Response with results ```json { "success": { - "status": 200, + "status": "OK", "message": "Found 2 items", - "code": "OK" + "code": 200 }, "results": [ { @@ -235,9 +269,9 @@ Response for get by id operation ```json { "success": { - "status": 200, + "status": "OK", "message": "object found", - "code": "OK" + "code": 200 }, "results": { "account_id": 4, @@ -252,9 +286,9 @@ Response with results and service-defined results tag `rpz_hits` ```json { "success": { - "status": 200, + "status": "OK", "message": "Read 360 items", - "code": "OK" + "code": 200 }, "rpz_hits": [ { @@ -304,33 +338,6 @@ operators using the one defined in [filter.go](filter.go). filter_Foobar_List_0 = gateway.DefaultQueryFilter ``` -## Errors - -### Format -Method error responses are rendered in the Error JSON format. The Error JSON format is similar to the Success JSON format. - -The Error JSON structure has the following format. The details tag is optional and appears when the service provides more details about the error. -```json -{ - "error": { - "status": , - "code": , - "message": - }, - "details": [ - { - "message": , - "code": , - "target": , - }, - ... - ], - "fields": { - "field1": [, , ...], - "field2": ..., - } -} -``` ### Translating gRPC Errors to HTTP @@ -370,6 +377,9 @@ You can find sample in example folder. See [code](example/cmd/gateway/main.go) The idiomatic way to send an error from you gRPC service is to simple return it from you gRPC handler either as `status.Errorf()` or `errors.New()`. +If additional fields are required, then use the +`NewResponseError(ctx context.Context, msg string, kvpairs ...interface{})` or +`NewResponseErrorWithCode(ctx context.Context, c codes.Code, msg string, kvpairs ...interface{})` functions instead. ```go import ( @@ -380,4 +390,8 @@ import ( func (s *myServiceImpl) MyMethod(req *MyRequest) (*MyResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method is not implemented: %v", req) } + +func (s *myServiceImpl) MyMethod2(ctx context.Context, req *MyRequest) (*MyResponse, error) { + return nil, NewResponseErrorWithCode(ctx, codes.Internal, "something broke on our end", "retry_in", 30) +} ``` diff --git a/gateway/errors.go b/gateway/errors.go index f4e9c5e3..8eb87fff 100644 --- a/gateway/errors.go +++ b/gateway/errors.go @@ -3,6 +3,7 @@ package gateway import ( "context" "encoding/json" + "errors" "fmt" "io" "math" @@ -87,12 +88,16 @@ func (h *ProtoErrorHandler) StreamHandler(ctx context.Context, headerWritten boo } func (h *ProtoErrorHandler) writeError(ctx context.Context, headerWritten bool, marshaler runtime.Marshaler, rw http.ResponseWriter, err error) { - const fallback = `{"error":[{"message":"%s"}]}` + var fallback = `{"error":[{"message":"%s"}]}` + if setStatusDetails { + fallback = `{"error":[{"message":"%s", "code":500, "status": "INTERNAL"}]}` + } st, ok := status.FromError(err) if !ok { st = status.New(codes.Unknown, err.Error()) } + statusCode, statusStr := HTTPStatus(ctx, st) details := []interface{}{} var fields interface{} @@ -119,16 +124,25 @@ func (h *ProtoErrorHandler) writeError(ctx context.Context, headerWritten bool, if fields != nil { restErr["fields"] = fields } + if setStatusDetails { + restErr["code"] = statusCode + restErr["status"] = statusStr + } - errs, _ := errorsAndSuccessFromContext(ctx) + errs, _, overrideErr := errorsAndSuccessFromContext(ctx) restResp := &RestErrs{ Error: errs, } - restResp.Error = append(restResp.Error, restErr) + if !overrideErr { + restResp.Error = append(restResp.Error, restErr) + } else if setStatusDetails && len(restResp.Error) > 0 { + restResp.Error[0]["code"] = statusCode + restResp.Error[0]["status"] = statusStr + } if !headerWritten { rw.Header().Del("Trailer") rw.Header().Set("Content-Type", marshaler.ContentType()) - rw.WriteHeader(HTTPStatus(ctx, st)) + rw.WriteHeader(statusCode) } buf, merr := marshaler.Marshal(restResp) @@ -210,6 +224,35 @@ func WithError(ctx context.Context, err error) { grpc.SetTrailer(ctx, md) } +// NewResponseError sets the error in the context with extra fields, to +// override the standard message-only error +func NewResponseError(ctx context.Context, msg string, kvpairs ...interface{}) error { + md := metadata.Pairs("error", fmt.Sprintf("message:%s", msg)) + if len(kvpairs) > 0 { + fields := make(map[string]interface{}) + for i := 0; i+1 < len(kvpairs); i += 2 { + k, ok := kvpairs[i].(string) + if !ok { + grpclog.Infof("Key value for error details must be a string") + continue + } + fields[k] = kvpairs[i+1] + } + b, _ := json.Marshal(fields) + md.Append("error", fmt.Sprintf("fields:%q", b)) + } + grpc.SetTrailer(ctx, md) + return errors.New(msg) // Message should be overridden in response writer +} + +// NewResponseErrorWithCode sets the return code and returns an error with extra +// fields in MD to be extracted in the gateway response writer +func NewResponseErrorWithCode(ctx context.Context, c codes.Code, msg string, kvpairs ...interface{}) error { + SetStatus(ctx, status.New(c, msg)) + NewResponseError(ctx, msg, kvpairs...) + return status.Error(c, msg) +} + // WithSuccess will save a MessageWithFields into the grpc trailer metadata. // This success message will then be inserted into the return JSON if the // ResponseForwarder is used @@ -223,14 +266,35 @@ func WithSuccess(ctx context.Context, msg MessageWithFields) { grpc.SetTrailer(ctx, md) } -func errorsAndSuccessFromContext(ctx context.Context) (errors []map[string]interface{}, success map[string]interface{}) { +// WithCodedSuccess wraps a SetStatus and WithSuccess call into one, just to make things a little more "elegant" +func WithCodedSuccess(ctx context.Context, c codes.Code, msg string, args ...interface{}) error { + WithSuccess(ctx, NewWithFields(msg, args)) + return SetStatus(ctx, status.New(c, msg)) +} + +func errorsAndSuccessFromContext(ctx context.Context) (errors []map[string]interface{}, success map[string]interface{}, errorOverride bool) { md, ok := runtime.ServerMetadataFromContext(ctx) if !ok { - return nil, nil + return nil, nil, false } errors = make([]map[string]interface{}, 0) + var primaryError map[string]interface{} latestSuccess := int64(-1) for k, vs := range md.TrailerMD { + if k == "error" { + err := make(map[string]interface{}) + for _, v := range vs { + parts := strings.SplitN(v, ":", 2) + if parts[0] == "fields" { + uq, _ := strconv.Unquote(parts[1]) + json.Unmarshal([]byte(uq), &err) + } else if parts[0] == "message" { + err["message"] = parts[1] + } + } + primaryError = err + errorOverride = true + } if strings.HasPrefix(k, "error-") { err := make(map[string]interface{}) for _, v := range vs { @@ -266,5 +330,8 @@ func errorsAndSuccessFromContext(ctx context.Context) (errors []map[string]inter } } } + if errorOverride { + errors = append([]map[string]interface{}{primaryError}, errors...) + } return } diff --git a/gateway/errors_test.go b/gateway/errors_test.go index 4e6fd7b7..55db1e95 100644 --- a/gateway/errors_test.go +++ b/gateway/errors_test.go @@ -1,6 +1,7 @@ package gateway import ( + "bytes" "context" "encoding/json" "fmt" @@ -13,6 +14,7 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -39,6 +41,53 @@ func TestProtoMessageErrorHandlerUnknownCode(t *testing.T) { } } +func TestProtoMessageErrorHandlerWithDetails(t *testing.T) { + IncludeStatusDetails(true) + defer IncludeStatusDetails(false) + err := status.Error(codes.NotFound, "overridden error") + v := &RestErrs{} + + b := &bytes.Buffer{} + enc := json.NewEncoder(b) + enc.Encode(map[string]interface{}{"bar": 2}) + md := runtime.ServerMetadata{ + HeaderMD: metadata.Pairs( + // "status-code", CodeName(codes.NotFound), + ), + TrailerMD: metadata.Pairs( + "error", "message:err message", + "error", fmt.Sprintf("fields:%q", string(b.Bytes()))), + } + ctx := runtime.NewServerMetadataContext(context.Background(), md) + + rw := httptest.NewRecorder() + ProtoMessageErrorHandler(ctx, nil, &runtime.JSONBuiltin{}, rw, nil, err) + + if ct := rw.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("invalid content-type: %s - expected: %s", ct, "application/json") + } + if rw.Code != http.StatusNotFound { + t.Errorf("invalid http status code: %d - expected: %d", rw.Code, http.StatusNotFound) + } + + if err := json.Unmarshal(rw.Body.Bytes(), v); err != nil { + t.Fatalf("failed to unmarshal response: %s", err) + } + + if v.Error[0]["message"] != "err message" { + t.Errorf("invalid message: %s", v.Error[0]["message"]) + } + if v.Error[0]["bar"].(float64) != 2 { + t.Errorf("invalid bar field: %d", v.Error[0]["bar"]) + } + if v.Error[0]["status"] != CodeName(codes.NotFound) { + t.Errorf("invalid status name: %s - expected: %s", v.Error[0]["status"], CodeName(codes.NotFound)) + } + if v.Error[0]["code"].(float64) != http.StatusNotFound { + t.Errorf("invalid status code: %d - expected: %d", v.Error[0]["code"], http.StatusNotFound) + } +} + func TestProtoMessageErrorHandlerUnimplementedCode(t *testing.T) { err := status.Error(codes.Unimplemented, "service not implemented") v := new(RestErrs) diff --git a/gateway/response.go b/gateway/response.go index cfe12263..4af8be11 100644 --- a/gateway/response.go +++ b/gateway/response.go @@ -36,8 +36,16 @@ var ( ForwardResponseMessage = NewForwardResponseMessage(PrefixOutgoingHeaderMatcher, ProtoMessageErrorHandler, ProtoStreamErrorHandler) // ForwardResponseStream is default implementation of ForwardResponseStreamFunc ForwardResponseStream = NewForwardResponseStream(PrefixOutgoingHeaderMatcher, ProtoMessageErrorHandler, ProtoStreamErrorHandler) + + setStatusDetails = false ) +// IncludeStatusDetails enables/disables output of status & code fields in all http json +// translated in the gateway with this package's ForwardResponseMessage +func IncludeStatusDetails(withDetails bool) { + setStatusDetails = withDetails +} + // NewForwardResponseMessage returns ForwardResponseMessageFunc func NewForwardResponseMessage(out runtime.HeaderMatcherFunc, meh runtime.ProtoErrorHandlerFunc, seh ProtoStreamErrorHandlerFunc) ForwardResponseMessageFunc { fw := &ResponseForwarder{out, meh, seh} @@ -89,19 +97,28 @@ func (fw *ResponseForwarder) ForwardMessage(ctx context.Context, mux *runtime.Se fw.MessageErrHandler(ctx, mux, marshaler, rw, req, err) } + httpStatus, statusStr := HTTPStatus(ctx, nil) + retainFields(ctx, req, dynmap) - errs, suc := errorsAndSuccessFromContext(ctx) + errs, suc, _ := errorsAndSuccessFromContext(ctx) if _, ok := dynmap["error"]; len(errs) > 0 && !ok { dynmap["error"] = errs } // this is the edge case, if user sends response that has field 'success' // let him see his response object instead of our status - if _, ok := dynmap["success"]; !ok && suc != nil { - dynmap["success"] = suc + if _, ok := dynmap["success"]; !ok { + if setStatusDetails { + if suc == nil { + suc = map[string]interface{}{} + } + suc["code"] = httpStatus + suc["status"] = statusStr + } + if suc != nil { + dynmap["success"] = suc + } } - httpStatus := HTTPStatus(ctx, nil) - data, err = json.Marshal(dynmap) if err != nil { grpclog.Infof("forward response: failed to marshal response: %v", err) @@ -143,7 +160,7 @@ func (fw *ResponseForwarder) ForwardStream(ctx context.Context, mux *runtime.Ser return } - httpStatus := HTTPStatus(ctx, nil) + httpStatus, _ := HTTPStatus(ctx, nil) // if user did not set status explicitly if httpStatus == http.StatusOK { httpStatus = HTTPStatusFromCode(PartialContent) diff --git a/gateway/response_test.go b/gateway/response_test.go index b6c0d700..74ad946a 100644 --- a/gateway/response_test.go +++ b/gateway/response_test.go @@ -63,7 +63,7 @@ type response struct { func TestForwardResponseMessage(t *testing.T) { b := &bytes.Buffer{} enc := json.NewEncoder(b) - enc.Encode(map[string]interface{}{"code": CodeName(Created), "status": 201}) + enc.Encode(map[string]interface{}{"code": 201, "status": CodeName(Created)}) md := runtime.ServerMetadata{ HeaderMD: metadata.Pairs( "grpcgateway-status-code", CodeName(Created), @@ -97,12 +97,12 @@ func TestForwardResponseMessage(t *testing.T) { t.Fatalf("failed to unmarshal JSON response: %s", err) } - if v.Success["code"] != CodeName(Created) { - t.Errorf("invalid status code: %s - expected: %s", v.Success["code"], CodeName(Created)) + if v.Success["status"] != CodeName(Created) { + t.Errorf("invalid status string: %s - expected: %s", v.Success["status"], CodeName(Created)) } - if v.Success["status"].(float64) != http.StatusCreated { - t.Errorf("invalid http status code: %d - expected: %d", v.Success["status"], http.StatusCreated) + if v.Success["code"].(float64) != http.StatusCreated { + t.Errorf("invalid http status code: %d - expected: %d", v.Success["code"], http.StatusCreated) } if v.Success["message"] != "created 1 item" { @@ -123,6 +123,136 @@ func TestForwardResponseMessage(t *testing.T) { } } +func TestForwardResponseMessageWithDetailsIncluded(t *testing.T) { + IncludeStatusDetails(true) + defer IncludeStatusDetails(false) + md := runtime.ServerMetadata{ + HeaderMD: metadata.Pairs( + "grpcgateway-status-code", CodeName(Created), + ), + TrailerMD: metadata.Pairs( + "success-1", "message:deleted 1 item", + "success-5", "message:created 1 item", + ), + } + ctx := runtime.NewServerMetadataContext(context.Background(), md) + rw := httptest.NewRecorder() + ForwardResponseMessage(ctx, nil, &runtime.JSONBuiltin{}, rw, nil, &result{Users: []*user{{"Poe", 209}, {"Hemingway", 119}}}) + + if rw.Code != http.StatusCreated { + t.Errorf("invalid http status code: %d - expected: %d", rw.Code, http.StatusCreated) + } + + if ct := rw.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("invalid content-type: %s - expected: %s", ct, "application/json") + } + + mdSt := "Grpc-Metadata-Grpcgateway-Status-Code" + if h := rw.Header().Get(mdSt); h != "" { + t.Errorf("got %s: %s", mdSt, h) + } + + v := &response{} + if err := json.Unmarshal(rw.Body.Bytes(), v); err != nil { + t.Fatalf("failed to unmarshal JSON response: %s", err) + } + + if v.Success["status"] != CodeName(Created) { + t.Errorf("invalid status string: %s - expected: %s", v.Success["status"], CodeName(Created)) + } + + if v.Success["code"].(float64) != http.StatusCreated { + t.Errorf("invalid http status code: %d - expected: %d", v.Success["code"], http.StatusCreated) + } + + if v.Success["message"] != "created 1 item" { + t.Errorf("invalid status message: %s - expected: %s", v.Success["message"], "created 1 item") + } + + if l := len(v.Result); l != 2 { + t.Fatalf("invalid number of items in response result: %d - expected: %d", l, 2) + } + + poe, hemingway := v.Result[0], v.Result[1] + if poe.Name != "Poe" || poe.Age != 209 { + t.Errorf("invalid result item: %+v - expected: %+v", poe, &user{"Poe", 209}) + } + + if hemingway.Name != "Hemingway" || hemingway.Age != 119 { + t.Errorf("invalid result item: %+v - expected: %+v", hemingway, &user{"Hemingway", 119}) + } +} + +func TestForwardResponseMessageWithErrorsAndDetailsIncluded(t *testing.T) { + IncludeStatusDetails(true) + defer IncludeStatusDetails(false) + b := &bytes.Buffer{} + enc := json.NewEncoder(b) + enc.Encode(map[string]interface{}{"bar": 2}) + md := runtime.ServerMetadata{ + HeaderMD: metadata.Pairs( + "grpcgateway-status-code", CodeName(Created), + ), + TrailerMD: metadata.Pairs( + "error-1", "message:err message", + "error-1", fmt.Sprintf("fields:%q", string(b.Bytes()))), + } + ctx := runtime.NewServerMetadataContext(context.Background(), md) + rw := httptest.NewRecorder() + ForwardResponseMessage(ctx, nil, &runtime.JSONBuiltin{}, rw, nil, &result{Users: []*user{{"Poe", 209}, {"Hemingway", 119}}}) + + if rw.Code != http.StatusCreated { + t.Errorf("invalid http status code: %d - expected: %d", rw.Code, http.StatusCreated) + } + + if ct := rw.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("invalid content-type: %s - expected: %s", ct, "application/json") + } + + mdSt := "Grpc-Metadata-Grpcgateway-Status-Code" + if h := rw.Header().Get(mdSt); h != "" { + t.Errorf("got %s: %s", mdSt, h) + } + + v := &response{} + if err := json.Unmarshal(rw.Body.Bytes(), v); err != nil { + t.Fatalf("failed to unmarshal JSON response: %s", err) + } + + if len(v.Error) != 1 { + t.Errorf("did not contain expected error in response") + } + + if v.Success["status"] != CodeName(Created) { + t.Errorf("invalid status string: %s - expected: %s", v.Success["status"], CodeName(Created)) + } + + if v.Success["code"].(float64) != http.StatusCreated { + t.Errorf("invalid http status code: %d - expected: %d", v.Success["code"], http.StatusCreated) + } + + if v.Error[0]["bar"].(float64) != 2 { + t.Errorf("unexpected err field: %d - expected: %d", v.Error[0]["bar"], 2) + } + + if v.Error[0]["message"] != "err message" { + t.Errorf("invalid status message: %s - expected: %s", v.Error[0]["message"], "err message") + } + + if l := len(v.Result); l != 2 { + t.Fatalf("invalid number of items in response result: %d - expected: %d", l, 2) + } + + poe, hemingway := v.Result[0], v.Result[1] + if poe.Name != "Poe" || poe.Age != 209 { + t.Errorf("invalid result item: %+v - expected: %+v", poe, &user{"Poe", 209}) + } + + if hemingway.Name != "Hemingway" || hemingway.Age != 119 { + t.Errorf("invalid result item: %+v - expected: %+v", hemingway, &user{"Hemingway", 119}) + } +} + func TestForwardResponseMessageWithNil(t *testing.T) { ctx := runtime.NewServerMetadataContext(context.Background(), runtime.ServerMetadata{}) diff --git a/gateway/status.go b/gateway/status.go index 17641bea..0a8271fa 100644 --- a/gateway/status.go +++ b/gateway/status.go @@ -66,25 +66,24 @@ func SetRunning(ctx context.Context, message, resource string) error { } // Status returns REST representation of gRPC status. -// If status.Status is not nil it will be converted in accrodance with REST +// If status.Status is not nil it will be converted in accordance with REST // API Syntax otherwise context will be used to extract -// `grpcgatewau-status-code` and `grpcgateway-status-message` from -// gRPC metadata. -// If `grpcgatewau-status-code` is not set it is assumed that it is OK. -func HTTPStatus(ctx context.Context, st *status.Status) int { +// `grpcgateway-status-code` from gRPC metadata. +// If `grpcgateway-status-code` is not set it is assumed that it is OK. +func HTTPStatus(ctx context.Context, st *status.Status) (int, string) { if st != nil { httpStatus := HTTPStatusFromCode(st.Code()) - return httpStatus + return httpStatus, CodeName(st.Code()) } - code := CodeName(codes.OK) + statusName := CodeName(codes.OK) if sc, ok := Header(ctx, "status-code"); ok { - code = sc + statusName = sc } - httpStatus := HTTPStatusFromCode(Code(code)) + httpCode := HTTPStatusFromCode(Code(statusName)) - return httpStatus + return httpCode, statusName } // CodeName returns stringname of gRPC code, function handles as standard diff --git a/gateway/status_test.go b/gateway/status_test.go index 0b1845d8..dae419ec 100644 --- a/gateway/status_test.go +++ b/gateway/status_test.go @@ -15,22 +15,28 @@ import ( func TestStatus(t *testing.T) { // test REST status from gRPC one - stat := HTTPStatus(context.Background(), status.New(codes.OK, "success message")) + stat, statName := HTTPStatus(context.Background(), status.New(codes.OK, "success message")) if stat != http.StatusOK { t.Errorf("invalid http status code %d - expected: %d", stat, http.StatusOK) } + if statName != codes.OK.String() { + t.Errorf("invalid http status codename %q - expected: %q", statName, codes.OK.String()) + } // test REST status from incoming context md := metadata.Pairs( runtime.MetadataPrefix+"status-code", CodeName(Created), ) ctx := metadata.NewIncomingContext(context.Background(), md) - stat = HTTPStatus(ctx, nil) + stat, statName = HTTPStatus(ctx, nil) if stat != http.StatusCreated { t.Errorf("invalid http status code %d - expected: %d", stat, http.StatusCreated) } + if statName != CodeName(Created) { + t.Errorf("invalid http status codename %q - expected: %q", statName, codes.OK.String()) + } } func TestCodeName(t *testing.T) {