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

feat: convert grpc errors to http status codes #1997

Merged
merged 3 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions rest/httpx/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync"

"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/internal/errcode"
"github.com/zeromicro/go-zero/rest/internal/header"
)

Expand All @@ -23,9 +24,14 @@ func Error(w http.ResponseWriter, err error, fns ...func(w http.ResponseWriter,
if handler == nil {
if len(fns) > 0 {
fns[0](w, err)
} else if errcode.IsGrpcError(err) {
// don't unwrap error and get status.Message(),
// it hides the rpc error headers.
http.Error(w, err.Error(), errcode.CodeFromGrpcError(err))
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
}

return
}

Expand Down
12 changes: 12 additions & 0 deletions rest/httpx/responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type message struct {
Expand Down Expand Up @@ -95,6 +97,16 @@ func TestError(t *testing.T) {
}
}

func TestErrorWithGrpcError(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),
}
Error(&w, status.Error(codes.Unavailable, "foo"))
assert.Equal(t, http.StatusServiceUnavailable, w.code)
assert.True(t, w.hasBody)
assert.True(t, strings.Contains(w.builder.String(), "foo"))
}

func TestErrorWithHandler(t *testing.T) {
w := tracedResponseWriter{
headers: make(map[string][]string),
Expand Down
55 changes: 55 additions & 0 deletions rest/internal/errcode/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package errcode

import (
"net/http"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// CodeFromGrpcError converts the gRPC error to an HTTP status code.
// See: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
func CodeFromGrpcError(err error) int {
code := status.Code(err)
switch code {
case codes.OK:
return http.StatusOK
case codes.InvalidArgument, codes.FailedPrecondition, codes.OutOfRange:
return http.StatusBadRequest
case codes.Unauthenticated:
return http.StatusUnauthorized
case codes.PermissionDenied:
return http.StatusForbidden
case codes.NotFound:
return http.StatusNotFound
case codes.Canceled:
return http.StatusRequestTimeout
case codes.AlreadyExists, codes.Aborted:
return http.StatusConflict
case codes.ResourceExhausted:
return http.StatusTooManyRequests
case codes.Internal, codes.DataLoss, codes.Unknown:
return http.StatusInternalServerError
case codes.Unimplemented:
return http.StatusNotImplemented
case codes.Unavailable:
return http.StatusServiceUnavailable
case codes.DeadlineExceeded:
return http.StatusGatewayTimeout
}

return http.StatusInternalServerError
}

// IsGrpcError checks if the error is a gRPC error.
func IsGrpcError(err error) bool {
if err == nil {
return false
}

_, ok := err.(interface {
GRPCStatus() *status.Status
})

return ok
}
123 changes: 123 additions & 0 deletions rest/internal/errcode/grpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package errcode

import (
"errors"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestCodeFromGrpcError(t *testing.T) {
tests := []struct {
name string
code codes.Code
want int
}{
{
name: "OK",
code: codes.OK,
want: http.StatusOK,
},
{
name: "Invalid argument",
code: codes.InvalidArgument,
want: http.StatusBadRequest,
},
{
name: "Failed precondition",
code: codes.FailedPrecondition,
want: http.StatusBadRequest,
},
{
name: "Out of range",
code: codes.OutOfRange,
want: http.StatusBadRequest,
},
{
name: "Unauthorized",
code: codes.Unauthenticated,
want: http.StatusUnauthorized,
},
{
name: "Permission denied",
code: codes.PermissionDenied,
want: http.StatusForbidden,
},
{
name: "Not found",
code: codes.NotFound,
want: http.StatusNotFound,
},
{
name: "Canceled",
code: codes.Canceled,
want: http.StatusRequestTimeout,
},
{
name: "Already exists",
code: codes.AlreadyExists,
want: http.StatusConflict,
},
{
name: "Aborted",
code: codes.Aborted,
want: http.StatusConflict,
},
{
name: "Resource exhausted",
code: codes.ResourceExhausted,
want: http.StatusTooManyRequests,
},
{
name: "Internal",
code: codes.Internal,
want: http.StatusInternalServerError,
},
{
name: "Data loss",
code: codes.DataLoss,
want: http.StatusInternalServerError,
},
{
name: "Unknown",
code: codes.Unknown,
want: http.StatusInternalServerError,
},
{
name: "Unimplemented",
code: codes.Unimplemented,
want: http.StatusNotImplemented,
},
{
name: "Unavailable",
code: codes.Unavailable,
want: http.StatusServiceUnavailable,
},
{
name: "Deadline exceeded",
code: codes.DeadlineExceeded,
want: http.StatusGatewayTimeout,
},
{
name: "Beyond defined error",
code: codes.Code(^uint32(0)),
want: http.StatusInternalServerError,
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, CodeFromGrpcError(status.Error(test.code, "foo")))
})
}
}

func TestIsGrpcError(t *testing.T) {
assert.True(t, IsGrpcError(status.Error(codes.Unknown, "foo")))
assert.False(t, IsGrpcError(errors.New("foo")))
assert.False(t, IsGrpcError(nil))
}
2 changes: 1 addition & 1 deletion zrpc/internal/codes/accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
// Acceptable checks if given error is acceptable.
func Acceptable(err error) bool {
switch status.Code(err) {
case codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss:
case codes.DeadlineExceeded, codes.Internal, codes.Unavailable, codes.DataLoss, codes.Unimplemented:
return false
default:
return true
Expand Down
1 change: 0 additions & 1 deletion zrpc/internal/serverinterceptors/timeoutinterceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func UnaryTimeoutInterceptor(timeout time.Duration) grpc.UnaryServerInterceptor
return resp, err
case <-ctx.Done():
err := ctx.Err()

if err == context.Canceled {
err = status.Error(codes.Canceled, err.Error())
} else if err == context.DeadlineExceeded {
Expand Down