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

Added a unary grpc client interceptor that will allow grpc clients to return errors that are compatible with simplerr as well as the grpc status.FromError() and status.Code() methods #12

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
11 changes: 2 additions & 9 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
# options for analysis running
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 2m

issues:
# Only report issues for changes since master
new-from-rev: origin/master
Expand Down Expand Up @@ -56,8 +51,8 @@ linters:
- gofmt
# Checks error handling
- errcheck
# Checks deadcode
- deadcode
# Checks unused code
Comment on lines -59 to -60
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deprecated, replaced by unused

- unused
# Linter for Go source code that specializes in simplifying a code
- gosimple
# Vet examines Go source code and reports suspicious constructs, such as Printf calls whose
Expand All @@ -67,5 +62,3 @@ linters:
- ineffassign
# Static code analytics
- staticcheck
# Finds unused struct fields
- structcheck
57 changes: 57 additions & 0 deletions ecosystem/grpc/client_interceptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package simplegrpc

import (
"context"
"github.com/lobocv/simplerr"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type attr int

const (
AttrGRPCMethod = attr(1)
AttrGRPCStatus = attr(2)
)

// ReturnSimpleErrors returns a unary client interceptor that converts errors returned by the client to simplerr compatible
// errors. The underlying grpc status and code can still be extracted using the same status.FromError() and status.Code() methods
func ReturnSimpleErrors(registry *Registry) grpc.UnaryClientInterceptor {

if registry == nil {
registry = defaultRegistry
}

return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {

// Call the gRPC method
err := invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
return nil
}

grpcCode := codes.Unknown
msg := err.Error()

serr := simplerr.New(msg).
Attr(AttrGRPCMethod, method)

// Check if the error is a gRPC status error
// The GRPC framework seems to always return grpc errors on the client side, even if the server does not
// Therefore, this block should always run
if st, ok := status.FromError(err); ok {
_ = serr.Attr(AttrGRPCStatus, st)

grpcCode = st.Code()
simplerrCode, _ := registry.getGRPCCode(grpcCode)
_ = serr.Code(simplerrCode)
}

return &grpcError{
SimpleError: serr,
code: grpcCode,
}

}
}
117 changes: 117 additions & 0 deletions ecosystem/grpc/client_interceptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package simplegrpc

import (
"context"
"fmt"
"github.com/lobocv/simplerr"
"github.com/lobocv/simplerr/ecosystem/grpc/internal/ping"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"net"
"testing"
)

type PingService struct {
err error
}

func (s *PingService) Ping(_ context.Context, _ *ping.PingRequest) (*ping.PingResponse, error) {
// Your implementation of the Ping method goes here
fmt.Println("Received Ping request")
if s.err != nil {
return nil, s.err
}
return &ping.PingResponse{}, nil
}

func setupServerAndClient(port int) (*PingService, ping.PingServiceClient) {

server := grpc.NewServer()
service := &PingService{err: status.Error(codes.NotFound, "test error")}
ping.RegisterPingServiceServer(server, service)

// Create a listener on TCP port 50051
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
panic(fmt.Sprintf("Error creating listener: %v", err))
}

go func() {
if err = server.Serve(listener); err != nil {
panic(fmt.Sprintf("Error serving: %v", err))
}
}()

defaultInverseMapping := DefaultInverseMapping()
defaultInverseMapping[codes.DataLoss] = simplerr.CodeResourceExhausted
GetDefaultRegistry().SetInverseMapping(defaultInverseMapping)

interceptor := ReturnSimpleErrors(nil)

conn, err := grpc.Dial(fmt.Sprintf(":%d", port),
grpc.WithUnaryInterceptor(interceptor),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
client := ping.NewPingServiceClient(conn)

return service, client
}

func TestClientInterceptor(t *testing.T) {

server, client := setupServerAndClient(50051)
_, err := client.Ping(context.Background(), &ping.PingRequest{})

require.True(t, simplerr.HasErrorCode(err, simplerr.CodeNotFound), "simplerror code can be detected")
require.Equal(t, codes.NotFound, status.Code(err), "grpc code can be detected with grpc status package")

st, ok := simplerr.GetAttribute(err, AttrGRPCStatus)
require.True(t, ok)
require.Equal(t, codes.NotFound, st.(*status.Status).Code(), "can get the grpc Status")

method, ok := simplerr.GetAttribute(err, AttrGRPCMethod)
require.True(t, ok)
require.Equal(t, "/ping.PingService/Ping", method, "can get the grpc method which errored")

// Test the custom added mapping
server.err = status.Error(codes.DataLoss, "test error")
_, err = client.Ping(context.Background(), &ping.PingRequest{})
require.True(t, simplerr.HasErrorCode(err, simplerr.CodeResourceExhausted), "simplerror code can be detected")

}

// When a non grpc error is returned, the client still returns a grpc error with code Unknown
// Our interceptor should still be able to detect attributes on the error
func TestClientInterceptorNotGPRCError(t *testing.T) {

server, client := setupServerAndClient(50052)
server.err = fmt.Errorf("not a grpc error")

_, err := client.Ping(context.Background(), &ping.PingRequest{})

require.True(t, simplerr.HasErrorCode(err, simplerr.CodeUnknown), "simplerror code can be detected")
require.Equal(t, codes.Unknown, status.Code(err), "grpc code can be detected with grpc status package")

st, ok := simplerr.GetAttribute(err, AttrGRPCStatus)
require.True(t, ok)
require.Equal(t, codes.Unknown, st.(*status.Status).Code(), "can get the grpc Status")

method, ok := simplerr.GetAttribute(err, AttrGRPCMethod)
require.True(t, ok)
require.Equal(t, "/ping.PingService/Ping", method, "can get the grpc method which errored")

}

func TestClientInterceptorNoError(t *testing.T) {
server, client := setupServerAndClient(50053)
server.err = nil

_, err := client.Ping(context.Background(), &ping.PingRequest{})
require.Nil(t, err)
}
53 changes: 53 additions & 0 deletions ecosystem/grpc/defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package simplegrpc

import (
"github.com/lobocv/simplerr"
"google.golang.org/grpc/codes"
)

var (
// defaultRegistry is a global registry used by default.
defaultRegistry = NewRegistry()
)

func GetDefaultRegistry() *Registry {
return defaultRegistry
}

// DefaultMapping returns the default mapping of SimpleError codes to gRPC error codes
func DefaultMapping() map[simplerr.Code]codes.Code {
m := map[simplerr.Code]codes.Code{
simplerr.CodeUnknown: codes.Unknown,
simplerr.CodeAlreadyExists: codes.AlreadyExists,
simplerr.CodeNotFound: codes.NotFound,
simplerr.CodeDeadlineExceeded: codes.DeadlineExceeded,
simplerr.CodeCanceled: codes.Canceled,
simplerr.CodeUnauthenticated: codes.Unauthenticated,
simplerr.CodePermissionDenied: codes.PermissionDenied,
simplerr.CodeNotImplemented: codes.Unimplemented,
simplerr.CodeInvalidArgument: codes.InvalidArgument,
simplerr.CodeResourceExhausted: codes.ResourceExhausted,
simplerr.CodeUnavailable: codes.Unavailable,
}

return m
}

// DefaultInverseMapping returns the default inverse mapping of gRPC error codes to SimpleError codes
func DefaultInverseMapping() map[codes.Code]simplerr.Code {
m := map[codes.Code]simplerr.Code{
codes.Unknown: simplerr.CodeUnknown,
codes.AlreadyExists: simplerr.CodeAlreadyExists,
codes.NotFound: simplerr.CodeNotFound,
codes.DeadlineExceeded: simplerr.CodeDeadlineExceeded,
codes.Canceled: simplerr.CodeCanceled,
codes.Unauthenticated: simplerr.CodeUnauthenticated,
codes.PermissionDenied: simplerr.CodePermissionDenied,
codes.Unimplemented: simplerr.CodeNotImplemented,
codes.InvalidArgument: simplerr.CodeInvalidArgument,
codes.ResourceExhausted: simplerr.CodeResourceExhausted,
codes.Unavailable: simplerr.CodeUnavailable,
}

return m
}
31 changes: 31 additions & 0 deletions ecosystem/grpc/grpc_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package simplegrpc

import (
"github.com/lobocv/simplerr"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// grpcError is a wrapper that exposes a SimpleError in a way that implements the gRPC status interface
// This is required because the grpc `status` library returns an error that does not implement unwrapping.
type grpcError struct {
*simplerr.SimpleError
code codes.Code
}

// Unwrap implement the interface required for error unwrapping
func (e *grpcError) Unwrap() error {
return e.SimpleError
}

// GRPCStatus implements an interface that the gRPC framework uses to return the gRPC status code
func (e *grpcError) GRPCStatus() *status.Status {
// If the status was attached as an attribute, return it
v, _ := simplerr.GetAttribute(e.SimpleError, AttrGRPCStatus)
st, ok := v.(*status.Status)
if ok {
return st
}

return status.New(e.code, e.Error())
}
Loading
Loading