feat: convert grpc errors to http status codes (#1997)

* feat: convert grpc errors to http status codes

* chore: circuit break include unimplemented grpc error

* chore: add reference link in comments
This commit is contained in:
Kevin Wan 2022-06-11 23:07:26 +08:00 committed by GitHub
parent db9a1f3e27
commit ed1c937998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 197 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import (
"sync" "sync"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/internal/errcode"
"github.com/zeromicro/go-zero/rest/internal/header" "github.com/zeromicro/go-zero/rest/internal/header"
) )
@ -23,9 +24,14 @@ func Error(w http.ResponseWriter, err error, fns ...func(w http.ResponseWriter,
if handler == nil { if handler == nil {
if len(fns) > 0 { if len(fns) > 0 {
fns[0](w, err) 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 { } else {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
} }
return return
} }

View File

@ -8,6 +8,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
type message struct { type message struct {
@ -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) { func TestErrorWithHandler(t *testing.T) {
w := tracedResponseWriter{ w := tracedResponseWriter{
headers: make(map[string][]string), headers: make(map[string][]string),

View File

@ -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
}

View File

@ -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))
}

View File

@ -8,7 +8,7 @@ import (
// Acceptable checks if given error is acceptable. // Acceptable checks if given error is acceptable.
func Acceptable(err error) bool { func Acceptable(err error) bool {
switch status.Code(err) { 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 return false
default: default:
return true return true

View File

@ -49,7 +49,6 @@ func UnaryTimeoutInterceptor(timeout time.Duration) grpc.UnaryServerInterceptor
return resp, err return resp, err
case <-ctx.Done(): case <-ctx.Done():
err := ctx.Err() err := ctx.Err()
if err == context.Canceled { if err == context.Canceled {
err = status.Error(codes.Canceled, err.Error()) err = status.Error(codes.Canceled, err.Error())
} else if err == context.DeadlineExceeded { } else if err == context.DeadlineExceeded {