Skip to content

Commit 9918975

Browse files
authored
Merge pull request #36 from filecoin-project/feat/typed-errors
Support error codes / typed errors
2 parents dff6592 + f4f3f0e commit 9918975

File tree

8 files changed

+207
-12
lines changed

8 files changed

+207
-12
lines changed

client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func NewClient(ctx context.Context, addr string, namespace string, handler inter
9696
type client struct {
9797
namespace string
9898
paramEncoders map[reflect.Type]ParamEncoder
99+
errors *Errors
99100

100101
doRequest func(context.Context, clientRequest) (clientResponse, error)
101102
exiting <-chan struct{}
@@ -130,6 +131,7 @@ func httpClient(ctx context.Context, addr string, namespace string, outs []inter
130131
c := client{
131132
namespace: namespace,
132133
paramEncoders: config.paramEncoders,
134+
errors: config.errors,
133135
}
134136

135137
stop := make(chan struct{})
@@ -212,6 +214,7 @@ func websocketClient(ctx context.Context, addr string, namespace string, outs []
212214
c := client{
213215
namespace: namespace,
214216
paramEncoders: config.paramEncoders,
217+
errors: config.errors,
215218
}
216219

217220
requests := make(chan clientRequest)
@@ -442,7 +445,8 @@ func (fn *rpcFunc) processResponse(resp clientResponse, rval reflect.Value) []re
442445
if fn.errOut != -1 {
443446
out[fn.errOut] = reflect.New(errorType).Elem()
444447
if resp.Error != nil {
445-
out[fn.errOut].Set(reflect.ValueOf(resp.Error))
448+
449+
out[fn.errOut].Set(resp.Error.val(fn.client.errors))
446450
}
447451
}
448452

@@ -548,7 +552,7 @@ func (fn *rpcFunc) handleRpcCall(args []reflect.Value) (results []reflect.Value)
548552

549553
retVal = func() reflect.Value { return val.Elem() }
550554
}
551-
retry := resp.Error != nil && resp.Error.Code == 2 && fn.retry
555+
retry := resp.Error != nil && resp.Error.Code == eTempWSError && fn.retry
552556
if !retry {
553557
break
554558
}

errors.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package jsonrpc
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
)
7+
8+
type Errors struct {
9+
byType map[reflect.Type]ErrorCode
10+
byCode map[ErrorCode]reflect.Type
11+
}
12+
13+
type ErrorCode int
14+
15+
const FirstUserCode = 2
16+
17+
func NewErrors() Errors {
18+
return Errors{
19+
byType: map[reflect.Type]ErrorCode{},
20+
byCode: map[ErrorCode]reflect.Type{},
21+
}
22+
}
23+
24+
func (e *Errors) Register(c ErrorCode, typ interface{}) {
25+
rt := reflect.TypeOf(typ).Elem()
26+
if !rt.Implements(errorType) {
27+
panic("can't register non-error types")
28+
}
29+
30+
e.byType[rt] = c
31+
e.byCode[c] = rt
32+
}
33+
34+
type marshalable interface {
35+
json.Marshaler
36+
json.Unmarshaler
37+
}

handler.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ type request struct {
5050
const DEFAULT_MAX_REQUEST_SIZE = 100 << 20 // 100 MiB
5151

5252
type respError struct {
53-
Code int `json:"code"`
54-
Message string `json:"message"`
53+
Code ErrorCode `json:"code"`
54+
Message string `json:"message"`
55+
Meta json.RawMessage `json:"meta,omitempty"`
5556
}
5657

5758
func (e *respError) Error() string {
@@ -61,6 +62,31 @@ func (e *respError) Error() string {
6162
return e.Message
6263
}
6364

65+
var marshalableRT = reflect.TypeOf(new(marshalable)).Elem()
66+
67+
func (e *respError) val(errors *Errors) reflect.Value {
68+
if errors != nil {
69+
t, ok := errors.byCode[e.Code]
70+
if ok {
71+
var v reflect.Value
72+
if t.Kind() == reflect.Ptr {
73+
v = reflect.New(t.Elem())
74+
} else {
75+
v = reflect.New(t)
76+
}
77+
if len(e.Meta) > 0 && v.Type().Implements(marshalableRT) {
78+
_ = v.Interface().(marshalable).UnmarshalJSON(e.Meta)
79+
}
80+
if t.Kind() != reflect.Ptr {
81+
v = v.Elem()
82+
}
83+
return v
84+
}
85+
}
86+
87+
return reflect.ValueOf(e)
88+
}
89+
6490
type response struct {
6591
Jsonrpc string `json:"jsonrpc"`
6692
Result interface{} `json:"result,omitempty"`
@@ -108,7 +134,7 @@ func (s *RPCServer) register(namespace string, r interface{}) {
108134

109135
// Handle
110136

111-
type rpcErrFunc func(w func(func(io.Writer)), req *request, code int, err error)
137+
type rpcErrFunc func(w func(func(io.Writer)), req *request, code ErrorCode, err error)
112138
type chanOut func(reflect.Value, int64) error
113139

114140
func (s *RPCServer) handleReader(ctx context.Context, r io.Reader, w io.Writer, rpcError rpcErrFunc) {
@@ -186,6 +212,30 @@ func (s *RPCServer) getSpan(ctx context.Context, req request) (context.Context,
186212
return ctx, nil
187213
}
188214

215+
func (s *RPCServer) createError(err error) *respError {
216+
var code ErrorCode = 1
217+
if s.errors != nil {
218+
c, ok := s.errors.byType[reflect.TypeOf(err)]
219+
if ok {
220+
code = c
221+
}
222+
}
223+
224+
out := &respError{
225+
Code: code,
226+
Message: err.(error).Error(),
227+
}
228+
229+
if m, ok := err.(marshalable); ok {
230+
meta, err := m.MarshalJSON()
231+
if err == nil {
232+
out.Meta = meta
233+
}
234+
}
235+
236+
return out
237+
}
238+
189239
func (s *RPCServer) handle(ctx context.Context, req request, w func(func(io.Writer)), rpcError rpcErrFunc, done func(keepCtx bool), chOut chanOut) {
190240
// Not sure if we need to sanitize the incoming req.Method or not.
191241
ctx, span := s.getSpan(ctx, req)
@@ -278,10 +328,8 @@ func (s *RPCServer) handle(ctx context.Context, req request, w func(func(io.Writ
278328
if err != nil {
279329
log.Warnf("error in RPC call to '%s': %+v", req.Method, err)
280330
stats.Record(ctx, metrics.RPCResponseError.M(1))
281-
resp.Error = &respError{
282-
Code: 1,
283-
Message: err.(error).Error(),
284-
}
331+
332+
resp.Error = s.createError(err.(error))
285333
}
286334
}
287335

options.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Config struct {
1515
timeout time.Duration
1616

1717
paramEncoders map[reflect.Type]ParamEncoder
18+
errors *Errors
1819

1920
noReconnect bool
2021
proxyConnFactory func(func() (*websocket.Conn, error)) func() (*websocket.Conn, error) // for testing
@@ -68,3 +69,9 @@ func WithParamEncoder(t interface{}, encoder ParamEncoder) func(c *Config) {
6869
c.paramEncoders[reflect.TypeOf(t).Elem()] = encoder
6970
}
7071
}
72+
73+
func WithErrors(es Errors) func(c *Config) {
74+
return func(c *Config) {
75+
c.errors = &es
76+
}
77+
}

options_server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type ParamDecoder func(ctx context.Context, json []byte) (reflect.Value, error)
1010
type ServerConfig struct {
1111
paramDecoders map[reflect.Type]ParamDecoder
1212
maxRequestSize int64
13+
errors *Errors
1314
}
1415

1516
type ServerOption func(c *ServerConfig)
@@ -32,3 +33,9 @@ func WithMaxRequestSize(max int64) ServerOption {
3233
c.maxRequestSize = max
3334
}
3435
}
36+
37+
func WithServerErrors(es Errors) ServerOption {
38+
return func(c *ServerConfig) {
39+
c.errors = &es
40+
}
41+
}

rpc_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
logging "github.com/ipfs/go-log/v2"
2121
"github.com/stretchr/testify/assert"
2222
"github.com/stretchr/testify/require"
23+
"golang.org/x/xerrors"
2324
)
2425

2526
func init() {
@@ -991,3 +992,90 @@ func readerDec(ctx context.Context, rin []byte) (reflect.Value, error) {
991992

992993
return reflect.ValueOf(readerRegistery[id]), nil
993994
}
995+
996+
type ErrSomethingBad struct{}
997+
998+
func (e ErrSomethingBad) Error() string {
999+
return "something bad has happened"
1000+
}
1001+
1002+
type ErrMyErr struct{ str string }
1003+
1004+
var _ error = ErrSomethingBad{}
1005+
1006+
func (e *ErrMyErr) UnmarshalJSON(data []byte) error {
1007+
return json.Unmarshal(data, &e.str)
1008+
}
1009+
1010+
func (e *ErrMyErr) MarshalJSON() ([]byte, error) {
1011+
return json.Marshal(e.str)
1012+
}
1013+
1014+
func (e *ErrMyErr) Error() string {
1015+
return fmt.Sprintf("this happened: %s", e.str)
1016+
}
1017+
1018+
type ErrHandler struct{}
1019+
1020+
func (h *ErrHandler) Test() error {
1021+
return ErrSomethingBad{}
1022+
}
1023+
1024+
func (h *ErrHandler) TestP() error {
1025+
return &ErrSomethingBad{}
1026+
}
1027+
1028+
func (h *ErrHandler) TestMy(s string) error {
1029+
return &ErrMyErr{
1030+
str: s,
1031+
}
1032+
}
1033+
1034+
func TestUserError(t *testing.T) {
1035+
// setup server
1036+
1037+
serverHandler := &ErrHandler{}
1038+
1039+
const (
1040+
EBad = iota + FirstUserCode
1041+
EBad2
1042+
EMy
1043+
)
1044+
1045+
errs := NewErrors()
1046+
errs.Register(EBad, new(ErrSomethingBad))
1047+
errs.Register(EBad2, new(*ErrSomethingBad))
1048+
errs.Register(EMy, new(*ErrMyErr))
1049+
1050+
rpcServer := NewServer(WithServerErrors(errs))
1051+
rpcServer.Register("ErrHandler", serverHandler)
1052+
1053+
// httptest stuff
1054+
testServ := httptest.NewServer(rpcServer)
1055+
defer testServ.Close()
1056+
1057+
// setup client
1058+
1059+
var client struct {
1060+
Test func() error
1061+
TestP func() error
1062+
TestMy func(s string) error
1063+
}
1064+
closer, err := NewMergeClient(context.Background(), "ws://"+testServ.Listener.Addr().String(), "ErrHandler", []interface{}{
1065+
&client,
1066+
}, nil, WithErrors(errs))
1067+
require.NoError(t, err)
1068+
1069+
e := client.Test()
1070+
require.True(t, xerrors.Is(e, ErrSomethingBad{}))
1071+
1072+
e = client.TestP()
1073+
require.True(t, xerrors.Is(e, &ErrSomethingBad{}))
1074+
1075+
e = client.TestMy("some event")
1076+
require.Error(t, e)
1077+
require.Equal(t, "this happened: some event", e.Error())
1078+
require.Equal(t, "this happened: some event", e.(*ErrMyErr).Error())
1079+
1080+
closer()
1081+
}

server.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
// RPCServer provides a jsonrpc 2.0 http server handler
2121
type RPCServer struct {
2222
methods map[string]rpcHandler
23+
errors *Errors
2324

2425
// aliasedMethods contains a map of alias:original method names.
2526
// These are used as fallbacks if a method is not found by the given method name.
@@ -42,6 +43,7 @@ func NewServer(opts ...ServerOption) *RPCServer {
4243
aliasedMethods: map[string]string{},
4344
paramDecoders: config.paramDecoders,
4445
maxRequestSize: config.maxRequestSize,
46+
errors: config.errors,
4547
}
4648
}
4749

@@ -91,7 +93,7 @@ func (s *RPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9193
s.handleReader(ctx, r.Body, w, rpcError)
9294
}
9395

94-
func rpcError(wf func(func(io.Writer)), req *request, code int, err error) {
96+
func rpcError(wf func(func(io.Writer)), req *request, code ErrorCode, err error) {
9597
log.Errorf("RPC Error: %s", err)
9698
wf(func(w io.Writer) {
9799
if hw, ok := w.(http.ResponseWriter); ok {

websocket.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const wsCancel = "xrpc.cancel"
1919
const chValue = "xrpc.ch.val"
2020
const chClose = "xrpc.ch.close"
2121

22+
const eTempWSError = -1111111
23+
2224
type frame struct {
2325
// common
2426
Jsonrpc string `json:"jsonrpc"`
@@ -451,7 +453,7 @@ func (c *wsConn) closeInFlight() {
451453
ID: id,
452454
Error: &respError{
453455
Message: "handler: websocket connection closed",
454-
Code: 2,
456+
Code: eTempWSError,
455457
},
456458
}
457459
}
@@ -635,7 +637,7 @@ func (c *wsConn) handleWsConn(ctx context.Context) {
635637
ID: *req.req.ID,
636638
Error: &respError{
637639
Message: "handler: websocket connection closed",
638-
Code: 2,
640+
Code: eTempWSError,
639641
},
640642
}
641643
c.writeLk.Unlock()

0 commit comments

Comments
 (0)