Skip to content

Commit

Permalink
refactor: json rpc error and error codec interface
Browse files Browse the repository at this point in the history
  • Loading branch information
akaladarshi committed Oct 22, 2024
1 parent 65441ad commit ef506b1
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 183 deletions.
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
7 changes: 3 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
122 changes: 20 additions & 102 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit ef506b1

Please sign in to comment.