diff --git a/client.go b/client.go index ba355ee..bc3dac6 100644 --- a/client.go +++ b/client.go @@ -71,7 +71,7 @@ type clientResponse struct { Jsonrpc string `json:"jsonrpc"` Result json.RawMessage `json:"result"` ID interface{} `json:"id"` - Error *respError `json:"error,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` } type makeChanSink func() (context.Context, func([]byte, bool)) diff --git a/errors.go b/errors.go index b720eb2..dd4dc75 100644 --- a/errors.go +++ b/errors.go @@ -59,8 +59,7 @@ type marshalable interface { json.Unmarshaler } -// ErrorWithData contains extra data to explain the error -type ErrorWithData interface { - Error() string // returns the message - ErrorData() interface{} // returns the error data +type ErrorCodec interface { + FromJSONRPCError(JSONRPCError) error + ToJSONRPCError() (JSONRPCError, error) } diff --git a/handler.go b/handler.go index 120d35c..68cdffc 100644 --- a/handler.go +++ b/handler.go @@ -5,7 +5,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "reflect" @@ -66,95 +65,6 @@ type request struct { // Configured by WithMaxRequestSize. const DEFAULT_MAX_REQUEST_SIZE = 100 << 20 // 100 MiB -type respError struct { - Code ErrorCode `json:"code"` - Message string `json:"message"` - Meta json.RawMessage `json:"meta,omitempty"` - Data interface{} `json:"data,omitempty"` -} - -func (e *respError) Error() string { - if e.Code >= -32768 && e.Code <= -32000 { - return fmt.Sprintf("RPC error (%d): %s", e.Code, e.Message) - } - return e.Message -} - -func (e *respError) ErrorData() interface{} { - return e.Data -} - -var ( - marshalableRT = reflect.TypeOf(new(marshalable)).Elem() - errorsRT = reflect.TypeOf(new(ErrorWithData)).Elem() -) - -func (e *respError) val(errors *Errors) reflect.Value { - if errors != nil { - t, ok := errors.byCode[e.Code] - if ok { - var v reflect.Value - if t.Kind() == reflect.Ptr { - v = reflect.New(t.Elem()) - } else { - v = reflect.New(t) - } - - if len(e.Meta) > 0 && v.Type().Implements(marshalableRT) { - _ = v.Interface().(marshalable).UnmarshalJSON(e.Meta) - } - - msgField := v.Elem().FieldByName("Message") - if msgField.IsValid() && msgField.CanSet() && msgField.Kind() == reflect.String { - msgField.SetString(e.Message) - } - - if v.Type().Implements(errorsRT) { - dataField := v.Elem().FieldByName("Data") - if dataField.IsValid() && dataField.CanSet() { - dataField.Set(reflect.ValueOf(e.Data)) - } - } - - if t.Kind() != reflect.Ptr { - v = v.Elem() - } - return v - } - } - - return reflect.ValueOf(e) -} - -type response struct { - Jsonrpc string `json:"jsonrpc"` - Result interface{} `json:"result,omitempty"` - ID interface{} `json:"id"` - Error *respError `json:"error,omitempty"` -} - -func (r response) MarshalJSON() ([]byte, error) { - // Custom marshal logic as per JSON-RPC 2.0 spec: - // > `result`: - // > This member is REQUIRED on success. - // > This member MUST NOT exist if there was an error invoking the method. - // - // > `error`: - // > This member is REQUIRED on error. - // > This member MUST NOT exist if there was no error triggered during invocation. - data := map[string]interface{}{ - "jsonrpc": r.Jsonrpc, - "id": r.ID, - } - - if r.Error != nil { - data["error"] = r.Error - } else { - data["result"] = r.Result - } - return json.Marshal(data) -} - type handler struct { methods map[string]methodHandler errors *Errors @@ -359,7 +269,7 @@ func (s *handler) getSpan(ctx context.Context, req request) (context.Context, *t return ctx, span } -func (s *handler) createError(err error) *respError { +func (s *handler) createError(err error) *JSONRPCError { var code ErrorCode = 1 if s.errors != nil { c, ok := s.errors.byType[reflect.TypeOf(err)] @@ -368,12 +278,20 @@ func (s *handler) createError(err error) *respError { } } - out := &respError{ + out := &JSONRPCError{ Code: code, Message: err.Error(), } - if m, ok := err.(marshalable); ok { + switch m := err.(type) { + case ErrorCodec: + o, err := m.ToJSONRPCError() + if err != nil { + log.Warnf("Failed to convert error to JSONRPCError: %v", err) + } else { + out = &o + } + case marshalable: meta, marshalErr := m.MarshalJSON() if marshalErr == nil { out.Meta = meta @@ -382,11 +300,6 @@ func (s *handler) createError(err error) *respError { } } - var ed ErrorWithData - if errors.As(err, &ed) { - out.Data = ed.ErrorData() - } - return out } @@ -536,14 +449,19 @@ func (s *handler) handle(ctx context.Context, req request, w func(func(io.Writer log.Warnf("failed to setup channel in RPC call to '%s': %+v", req.Method, err) stats.Record(ctx, metrics.RPCResponseError.M(1)) - respErr := &respError{ + + respErr := &JSONRPCError{ Code: 1, Message: err.Error(), } - var ed ErrorWithData - if errors.As(err, &ed) { - respErr.Data = ed.ErrorData() + if m, ok := err.(ErrorCodec); ok { + respErr, err := m.ToJSONRPCError() + if err != nil { + log.Warnf("Failed to convert error to JSONRPCError: %v", err) + } else { + resp.Error.Data = respErr.Data + } } resp.Error = respErr diff --git a/resp_error_test.go b/resp_error_test.go index 85a278a..0e56814 100644 --- a/resp_error_test.go +++ b/resp_error_test.go @@ -2,12 +2,21 @@ package jsonrpc import ( "encoding/json" - "reflect" + "fmt" "testing" "github.com/stretchr/testify/require" ) +type ComplexData struct { + Foo string `json:"foo"` + Bar int `json:"bar"` +} + +type StaticError struct{} + +func (e *StaticError) Error() string { return "static error" } + // Define the error types type SimpleError struct { Message string @@ -17,19 +26,76 @@ func (e *SimpleError) Error() string { return e.Message } -type DataError struct { - Message string - Data interface{} +func (e *SimpleError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + return nil +} + +func (e *SimpleError) ToJSONRPCError() (JSONRPCError, error) { + return JSONRPCError{Message: e.Message}, nil } -func (e *DataError) Error() string { +var _ ErrorCodec = (*SimpleError)(nil) + +type DataStringError struct { + Message string `json:"message"` + Data string `json:"data"` +} + +func (e *DataStringError) Error() string { return e.Message } -func (e *DataError) ErrorData() interface{} { - return e.Data +func (e *DataStringError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + data, ok := jerr.Data.(string) + if !ok { + return fmt.Errorf("expected string data, got %T", jerr.Data) + } + + e.Data = data + + return nil +} + +func (e *DataStringError) ToJSONRPCError() (JSONRPCError, error) { + return JSONRPCError{Message: e.Message, Data: e.Data}, nil +} + +var _ ErrorCodec = (*DataStringError)(nil) + +type DataComplexError struct { + Message string + internalData ComplexData +} + +func (e *DataComplexError) Error() string { + return e.Message +} + +func (e *DataComplexError) FromJSONRPCError(jerr JSONRPCError) error { + e.Message = jerr.Message + data, ok := jerr.Data.(json.RawMessage) + if !ok { + return fmt.Errorf("expected string data, got %T", jerr.Data) + } + + if err := json.Unmarshal(data, &e.internalData); err != nil { + return err + } + return nil +} + +func (e *DataComplexError) ToJSONRPCError() (JSONRPCError, error) { + data, err := json.Marshal(e.internalData) + if err != nil { + return JSONRPCError{}, err + } + return JSONRPCError{Message: e.Message, Data: data}, nil } +var _ ErrorCodec = (*DataComplexError)(nil) + type MetaError struct { Message string Details string @@ -41,26 +107,31 @@ func (e *MetaError) Error() string { func (e *MetaError) MarshalJSON() ([]byte, error) { return json.Marshal(struct { + Message string `json:"message"` Details string `json:"details"` }{ + Message: e.Message, Details: e.Details, }) } func (e *MetaError) UnmarshalJSON(data []byte) error { var temp struct { + Message string `json:"message"` Details string `json:"details"` } if err := json.Unmarshal(data, &temp); err != nil { return err } + + e.Message = temp.Message e.Details = temp.Details return nil } type ComplexError struct { Message string - Data interface{} + Data ComplexData Details string } @@ -68,15 +139,11 @@ func (e *ComplexError) Error() string { return e.Message } -func (e *ComplexError) ErrorData() interface{} { - return e.Data -} - func (e *ComplexError) MarshalJSON() ([]byte, error) { return json.Marshal(struct { - Message string `json:"message"` - Details string `json:"details"` - Data interface{} `json:"data"` + Message string `json:"message"` + Details string `json:"details"` + Data any `json:"data"` }{ Details: e.Details, Message: e.Message, @@ -88,7 +155,7 @@ func (e *ComplexError) UnmarshalJSON(data []byte) error { var temp struct { Message string `json:"message"` Details string `json:"details"` - Data interface{} `json:"data"` + Data ComplexData `json:"data"` } if err := json.Unmarshal(data, &temp); err != nil { return err @@ -102,123 +169,138 @@ func (e *ComplexError) UnmarshalJSON(data []byte) error { func TestRespErrorVal(t *testing.T) { // Initialize the Errors struct and register error types errorsMap := NewErrors() + errorsMap.Register(1000, new(*StaticError)) errorsMap.Register(1001, new(*SimpleError)) - errorsMap.Register(1002, new(*DataError)) - errorsMap.Register(1003, new(*MetaError)) - errorsMap.Register(1004, new(*ComplexError)) + errorsMap.Register(1002, new(*DataStringError)) + errorsMap.Register(1003, new(*DataComplexError)) + errorsMap.Register(1004, new(*MetaError)) + errorsMap.Register(1005, new(*ComplexError)) // Define test cases testCases := []struct { - name string - respError *respError - expectedType reflect.Type - verify func(err error) error + name string + respError *JSONRPCError + expectedType interface{} + expectedMessage string + verify func(t *testing.T, err error) }{ + { + name: "StaticError", + respError: &JSONRPCError{ + Code: 1000, + Message: "this is ignored", + }, + expectedType: &StaticError{}, + expectedMessage: "static error", + }, { name: "SimpleError", - respError: &respError{ + respError: &JSONRPCError{ Code: 1001, Message: "simple error occurred", }, - expectedType: reflect.TypeOf(&SimpleError{}), - verify: func(err error) error { - require.IsType(t, &SimpleError{}, err) - require.Equal(t, "simple error occurred", err.Error()) - return nil - }, + expectedType: &SimpleError{}, + expectedMessage: "simple error occurred", }, { - name: "DataError", - respError: &respError{ + name: "DataStringError", + respError: &JSONRPCError{ Code: 1002, Message: "data error occurred", Data: "additional data", }, - expectedType: reflect.TypeOf(&DataError{}), - verify: func(err error) error { - require.IsType(t, &DataError{}, err) + expectedType: &DataStringError{}, + expectedMessage: "data error occurred", + verify: func(t *testing.T, err error) { + require.IsType(t, &DataStringError{}, err) require.Equal(t, "data error occurred", err.Error()) - require.Equal(t, "additional data", err.(*DataError).ErrorData()) - return nil + require.Equal(t, "additional data", err.(*DataStringError).Data) }, }, { - name: "MetaError", - respError: &respError{ + name: "DataComplexError", + respError: &JSONRPCError{ Code: 1003, + Message: "data error occurred", + Data: json.RawMessage(`{"foo":"boop","bar":101}`), + }, + expectedType: &DataComplexError{}, + expectedMessage: "data error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, ComplexData{Foo: "boop", Bar: 101}, err.(*DataComplexError).internalData) + }, + }, + { + name: "MetaError", + respError: &JSONRPCError{ + Code: 1004, Message: "meta error occurred", Meta: func() json.RawMessage { me := &MetaError{ + Message: "meta error occurred", Details: "meta details", } metaData, _ := me.MarshalJSON() return metaData }(), }, - expectedType: reflect.TypeOf(&MetaError{}), - verify: func(err error) error { - require.IsType(t, &MetaError{}, err) - require.Equal(t, "meta error occurred", err.Error()) + expectedType: &MetaError{}, + expectedMessage: "meta error occurred", + verify: func(t *testing.T, err error) { // details will also be included in the error message since it implements the marshable interface require.Equal(t, "meta details", err.(*MetaError).Details) - return nil }, }, { name: "ComplexError", - respError: &respError{ - Code: 1004, + respError: &JSONRPCError{ + Code: 1005, Message: "complex error occurred", - Data: "complex data", + Data: json.RawMessage(`"complex data"`), Meta: func() json.RawMessage { ce := &ComplexError{ + Message: "complex error occurred", Details: "complex details", + Data: ComplexData{Foo: "foo", Bar: 42}, } metaData, _ := ce.MarshalJSON() return metaData }(), }, - expectedType: reflect.TypeOf(&ComplexError{}), - verify: func(err error) error { - require.IsType(t, &ComplexError{}, err) - require.Equal(t, "complex error occurred", err.Error()) - require.Equal(t, "complex data", err.(*ComplexError).ErrorData()) + expectedType: &ComplexError{}, + expectedMessage: "complex error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, ComplexData{Foo: "foo", Bar: 42}, err.(*ComplexError).Data) require.Equal(t, "complex details", err.(*ComplexError).Details) - return nil }, }, { name: "UnregisteredError", - respError: &respError{ + respError: &JSONRPCError{ Code: 9999, Message: "unregistered error occurred", - Data: "some data", + Data: json.RawMessage(`"some data"`), }, - expectedType: reflect.TypeOf(&respError{}), - verify: func(err error) error { - require.IsType(t, &respError{}, err) - require.Equal(t, "unregistered error occurred", err.Error()) - require.Equal(t, "some data", err.(*respError).ErrorData()) - return nil + expectedType: &JSONRPCError{}, + expectedMessage: "unregistered error occurred", + verify: func(t *testing.T, err error) { + require.Equal(t, json.RawMessage(`"some data"`), err.(*JSONRPCError).Data) }, }, } for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { errValue := tc.respError.val(&errorsMap) errInterface := errValue.Interface() err, ok := errInterface.(error) - if !ok { - t.Fatalf("returned value does not implement error interface") + require.True(t, ok, "returned value does not implement error interface") + require.IsType(t, tc.expectedType, err) + require.Equal(t, tc.expectedMessage, err.Error()) + if tc.verify != nil { + tc.verify(t, err) } - - if reflect.TypeOf(err) != tc.expectedType { - t.Errorf("expected error type %v, got %v", tc.expectedType, reflect.TypeOf(err)) - } - - err = tc.verify(err) - require.NoError(t, err, "failed to verify error") }) } } diff --git a/response.go b/response.go new file mode 100644 index 0000000..72f92b4 --- /dev/null +++ b/response.go @@ -0,0 +1,85 @@ +package jsonrpc + +import ( + "encoding/json" + "fmt" + "reflect" +) + +type response struct { + Jsonrpc string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + ID interface{} `json:"id"` + Error *JSONRPCError `json:"error,omitempty"` +} + +func (r response) MarshalJSON() ([]byte, error) { + // Custom marshal logic as per JSON-RPC 2.0 spec: + // > `result`: + // > This member is REQUIRED on success. + // > This member MUST NOT exist if there was an error invoking the method. + // + // > `error`: + // > This member is REQUIRED on error. + // > This member MUST NOT exist if there was no error triggered during invocation. + data := map[string]interface{}{ + "jsonrpc": r.Jsonrpc, + "id": r.ID, + } + + if r.Error != nil { + data["error"] = r.Error + } else { + data["result"] = r.Result + } + return json.Marshal(data) +} + +type JSONRPCError struct { + Code ErrorCode `json:"code"` + Message string `json:"message"` + Meta json.RawMessage `json:"meta,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +func (e *JSONRPCError) Error() string { + if e.Code >= -32768 && e.Code <= -32000 { + return fmt.Sprintf("RPC error (%d): %s", e.Code, e.Message) + } + return e.Message +} + +var ( + _ error = (*JSONRPCError)(nil) + marshalableRT = reflect.TypeOf(new(marshalable)).Elem() + unmarshalableRT = reflect.TypeOf(new(ErrorCodec)).Elem() +) + +func (e *JSONRPCError) val(errors *Errors) reflect.Value { + if errors != nil { + t, ok := errors.byCode[e.Code] + if ok { + var v reflect.Value + if t.Kind() == reflect.Ptr { + v = reflect.New(t.Elem()) + } else { + v = reflect.New(t) + } + + if v.Type().Implements(unmarshalableRT) { + _ = v.Interface().(ErrorCodec).FromJSONRPCError(*e) + } + + if len(e.Meta) > 0 && v.Type().Implements(marshalableRT) { + _ = v.Interface().(marshalable).UnmarshalJSON(e.Meta) + } + + if t.Kind() != reflect.Ptr { + v = v.Elem() + } + return v + } + } + + return reflect.ValueOf(e) +} diff --git a/server.go b/server.go index cc2586a..4454c85 100644 --- a/server.go +++ b/server.go @@ -155,7 +155,7 @@ func rpcError(wf func(func(io.Writer)), req *request, code ErrorCode, err error) resp := response{ Jsonrpc: "2.0", ID: req.ID, - Error: &respError{ + Error: &JSONRPCError{ Code: code, Message: err.Error(), }, @@ -180,4 +180,4 @@ func (s *RPCServer) AliasMethod(alias, original string) { s.aliasedMethods[alias] = original } -var _ error = &respError{} +var _ error = &JSONRPCError{} diff --git a/websocket.go b/websocket.go index 60a0451..05755d3 100644 --- a/websocket.go +++ b/websocket.go @@ -34,7 +34,7 @@ type frame struct { // response Result json.RawMessage `json:"result,omitempty"` - Error *respError `json:"error,omitempty"` + Error *JSONRPCError `json:"error,omitempty"` } type outChanReg struct { @@ -529,7 +529,7 @@ func (c *wsConn) closeInFlight() { req.ready <- clientResponse{ Jsonrpc: "2.0", ID: id, - Error: &respError{ + Error: &JSONRPCError{ Message: "handler: websocket connection closed", Code: eTempWSError, }, @@ -802,7 +802,7 @@ func (c *wsConn) handleWsConn(ctx context.Context) { req.ready <- clientResponse{ Jsonrpc: "2.0", ID: req.req.ID, - Error: &respError{ + Error: &JSONRPCError{ Message: "handler: websocket connection closed", Code: eTempWSError, }, @@ -824,7 +824,7 @@ func (c *wsConn) handleWsConn(ctx context.Context) { Jsonrpc: "2.0", } if serr != nil { - resp.Error = &respError{ + resp.Error = &JSONRPCError{ Code: eTempWSError, Message: fmt.Sprintf("sendRequest: %s", serr), }