mirror of
https://github.com/zeromicro/go-zero.git
synced 2025-01-23 09:00:20 +08:00
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:
parent
db9a1f3e27
commit
ed1c937998
@ -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"
|
||||
)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
@ -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),
|
||||
|
55
rest/internal/errcode/grpc.go
Normal file
55
rest/internal/errcode/grpc.go
Normal 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
|
||||
}
|
123
rest/internal/errcode/grpc_test.go
Normal file
123
rest/internal/errcode/grpc_test.go
Normal 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))
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user