Skip to content

Commit

Permalink
Improve error handling in RPC transport
Browse files Browse the repository at this point in the history
  • Loading branch information
MDobak committed Sep 15, 2023
1 parent 6618102 commit 88f1784
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 53 deletions.
9 changes: 5 additions & 4 deletions abi/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type Contract struct {
Errors map[string]*Error
}

// IsError returns true if the given data returned by a contract call is a
// revert, panic or a custom error.
// IsError returns true if the given error data returned by a contract call is
// a revert, panic or a custom error.
func (c *Contract) IsError(data []byte) bool {
if IsRevert(data) || IsPanic(data) {
return true
Expand All @@ -35,8 +35,9 @@ func (c *Contract) IsError(data []byte) bool {
return false
}

// ToError returns error if the given data returned by a contract call is a
// revert, panic or a custom error.
// ToError returns error if the given error data returned by a contract call is
// a revert, panic or a custom error. It returns nil if the data cannot be
// recognized as an error.
func (c *Contract) ToError(data []byte) error {
if IsRevert(data) {
return RevertError{Reason: DecodeRevert(data)}
Expand Down
8 changes: 4 additions & 4 deletions hexutil/hexutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func HexToBigInt(h string) (*big.Int, error) {
if isNeg {
h = h[1:]
}
if has0xPrefix(h) {
if Has0xPrefix(h) {
h = h[2:]
}
x, ok := new(big.Int).SetString(h, 16)
Expand Down Expand Up @@ -68,7 +68,7 @@ func HexToBytes(h string) ([]byte, error) {
if len(h) == 0 {
return []byte{}, nil
}
if has0xPrefix(h) {
if Has0xPrefix(h) {
h = h[2:]
}
if len(h) == 1 && h[0] == '0' {
Expand All @@ -91,7 +91,7 @@ func MustHexToBytes(h string) []byte {
return b
}

// has0xPrefix returns true if the given byte slice starts with "0x".
func has0xPrefix(h string) bool {
// Has0xPrefix returns true if the given byte slice starts with "0x".
func Has0xPrefix(h string) bool {
return len(h) >= 2 && h[0] == '0' && (h[1] == 'x' || h[1] == 'X')
}
120 changes: 120 additions & 0 deletions rpc/transport/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package transport

import (
"fmt"
"net/http"

"github.com/defiweb/go-eth/hexutil"
)

const (
// Standard errors:
ErrCodeUnauthorized = 1
ErrCodeActionNotAllowed = 2
ErrCodeExecutionError = 3
ErrCodeParseError = -32700
ErrCodeInvalidRequest = -32600
ErrCodeMethodNotFound = -32601
ErrCodeInvalidParams = -32602
ErrCodeInternalError = -32603

// Common non-standard errors:
ErrCodeGeneral = -32000
ErrCodeLimitExceeded = -32005

// Erigon errors:
ErigonErrCodeGeneral = -32000
ErigonErrCodeNotFound = -32601
ErigonErrCodeUnsupportedFork = -38005

// Nethermind errors:
NethermindErrCodeMethodNotSupported = -32004
NethermindErrCodeLimitExceeded = -32005
NethermindErrCodeTransactionRejected = -32010
NethermindErrCodeExecutionError = -32015
NethermindErrCodeTimeout = -32016
NethermindErrCodeModuleTimeout = -32017
NethermindErrCodeAccountLocked = -32020
NethermindErrCodeUnknownBlockError = -39001

// Infura errors:
InfuraErrCodeInvalidInput = -32000
InfuraErrCodeResourceNotFound = -32001
InfuraErrCodeResourceUnavailable = -32002
InfuraErrCodeTransactionRejected = -32003
InfuraErrCodeMethodNotSupported = -32004
InfuraErrCodeLimitExceeded = -32005
InfuraErrCodeJSONRPCVersionNotSupported = -32006

// Alchemy errors:
AlchemyErrCodeLimitExceeded = 429

// Blast errors:
BlastErrCodeAuthenticationFailed = -32099
BlastErrCodeCapacityExceeded = -32098
BlastErrRateLimitReached = -32097
)

// RPCError is an JSON-RPC error.
type RPCError struct {
Code int // Code is the JSON-RPC error code.
Message string // Message is the error message.
Data any // Data associated with the error.
}

// NewRPCError creates a new RPC error.
//
// If data is a hex-encoded string, it will be decoded.
func NewRPCError(code int, message string, data any) *RPCError {
if bin, ok := decodeHexData(data); ok {
data = bin
}
return &RPCError{
Code: code,
Message: message,
Data: data,
}
}

// Error implements the error interface.
func (e *RPCError) Error() string {
return fmt.Sprintf("RPC error: %d %s", e.Code, e.Message)
}

// HTTPError is an HTTP error.
type HTTPError struct {
Code int // Code is the HTTP status code.
Err error // Err is an optional underlying error.
}

// NewHTTPError creates a new HTTP error.
func NewHTTPError(code int, err error) *HTTPError {
return &HTTPError{
Code: code,
Err: err,
}
}

// Error implements the error interface.
func (e *HTTPError) Error() string {
if e.Err == nil {
return fmt.Sprintf("HTTP error: %d %s", e.Code, http.StatusText(e.Code))
}
return fmt.Sprintf("HTTP error: %d %s: %s", e.Code, http.StatusText(e.Code), e.Err)
}

// decodeHexData decodes hex-encoded data if present.
func decodeHexData(data any) (any, bool) {
hex, ok := data.(string)
if !ok {
return nil, false
}
if !hexutil.Has0xPrefix(hex) {
return nil, false
}
bin, err := hexutil.HexToBytes(hex)
if err != nil {
return nil, false
}
return bin, true
}
50 changes: 50 additions & 0 deletions rpc/transport/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package transport

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/defiweb/go-eth/hexutil"
)

func TestNewRPCError(t *testing.T) {
tests := []struct {
name string
code int
message string
data any
expected *RPCError
}{
{
name: "error with non-hex data",
code: ErrCodeGeneral,
message: "Unauthorized access",
data: "some data",
expected: &RPCError{
Code: ErrCodeGeneral,
Message: "Unauthorized access",
Data: "some data",
},
},
{
name: "error with hex data",
code: ErrCodeGeneral,
message: "Invalid request",
data: "0x68656c6c6f",
expected: &RPCError{
Code: ErrCodeGeneral,
Message: "Invalid request",
Data: hexutil.MustHexToBytes("0x68656c6c6f"),
},
},
// Add more test cases as needed
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := NewRPCError(tt.code, tt.message, tt.data)
assert.Equal(t, tt.expected, actual)
})
}
}
12 changes: 6 additions & 6 deletions rpc/transport/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ func (h *HTTP) Call(ctx context.Context, result any, method string, args ...any)
if err := json.NewDecoder(httpRes.Body).Decode(rpcRes); err != nil {
// If the response is not a valid JSON-RPC response, return the HTTP
// status code as the error code.
return &HTTPError{Code: httpRes.StatusCode}
return NewHTTPError(httpRes.StatusCode, nil)
}
if rpcRes.Error != nil {
return &RPCError{
Code: rpcRes.Error.Code,
Message: rpcRes.Error.Message,
Data: rpcRes.Error.Data,
}
return NewRPCError(
rpcRes.Error.Code,
rpcRes.Error.Message,
rpcRes.Error.Data,
)
}
if result == nil {
return nil
Expand Down
30 changes: 23 additions & 7 deletions rpc/transport/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,57 @@ import (
"encoding/json"
"errors"
"math"
"strings"
"time"
)

var ErrNotSubscriptionTransport = errors.New("transport does not implement SubscriptionTransport")

var (
// RetryOnAnyError retries on any error except for the following:
// 3: Execution error.
// -32700: Parse error.
// -32600: Invalid request.
// -32601: Method not found.
// -32602: Invalid params.
// -32000: If error message starts with "execution reverted".
RetryOnAnyError = func(err error) bool {
// List of errors that should not be retried:
switch errorCode(err) {
case -32700: // Parse error.
case ErrCodeExecutionError:
return false
case -32600: // Invalid request.
case ErrCodeParseError:
return false
case -32601: // Method not found.
case ErrCodeInvalidRequest:
return false
case -32602: // Invalid params.
case ErrCodeMethodNotFound:
return false
case ErrCodeInvalidParams:
return false
case ErrCodeGeneral:
rpcErr := &RPCError{}
if errors.As(err, &rpcErr) {
if strings.HasPrefix(rpcErr.Message, "execution reverted") {
return false
}
}
}

// Retry on all other errors:
return err != nil
}

// RetryOnLimitExceeded retries on the following errors:
// -32005: Limit exceeded.
// 429: Too many requests.
// -32097: Rate limit reached (Blast).
// 429: Too many requests
RetryOnLimitExceeded = func(err error) bool {
switch errorCode(err) {
case -32005: // Limit exceeded.
case ErrCodeLimitExceeded:
return true
case BlastErrRateLimitReached:
return true
case 429: // Too many requests.
case AlchemyErrCodeLimitExceeded:
return true
}
return false
Expand Down
8 changes: 8 additions & 0 deletions rpc/transport/retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,14 @@ func TestRetryOnAnyError(t *testing.T) {
err: &RPCError{Code: -32005},
want: true,
},
{
err: &RPCError{Code: -32000, Message: "foo"},
want: true,
},
{
err: &RPCError{Code: -32000, Message: "execution reverted"},
want: false,
},
}
for n, test := range tests {
t.Run(fmt.Sprintf("case-%d", n+1), func(t *testing.T) {
Expand Down
10 changes: 5 additions & 5 deletions rpc/transport/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ func (s *stream) Call(ctx context.Context, result any, method string, args ...an
select {
case res := <-ch:
if res.Error != nil {
return &RPCError{
Code: res.Error.Code,
Message: res.Error.Message,
Data: res.Error.Data,
}
return NewRPCError(
res.Error.Code,
res.Error.Message,
res.Error.Data,
)
}
if result != nil {
if err := json.Unmarshal(res.Result, result); err != nil {
Expand Down
27 changes: 0 additions & 27 deletions rpc/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
netURL "net/url"
)

Expand Down Expand Up @@ -49,29 +48,3 @@ func New(ctx context.Context, rpcURL string) (Transport, error) {
return nil, fmt.Errorf("unsupported scheme: %s", url.Scheme)
}
}

// RPCError is an JSON-RPC error.
type RPCError struct {
Code int // Code is the JSON-RPC error code.
Message string // Message is the error message.
Data any // Data associated with the error.
}

// Error implements the error interface.
func (e *RPCError) Error() string {
return fmt.Sprintf("RPC error: %d %s", e.Code, e.Message)
}

// HTTPError is an HTTP error.
type HTTPError struct {
Code int // Code is the HTTP status code.
Err error // Err is an optional underlying error.
}

// Error implements the error interface.
func (e *HTTPError) Error() string {
if e.Err == nil {
return fmt.Sprintf("HTTP error: %d %s", e.Code, http.StatusText(e.Code))
}
return fmt.Sprintf("HTTP error: %d %s: %s", e.Code, http.StatusText(e.Code), e.Err)
}

0 comments on commit 88f1784

Please sign in to comment.