Skip to content

Commit e176fe3

Browse files
committed
add initial error framework
1 parent d0cfd45 commit e176fe3

13 files changed

+452
-133
lines changed

access/errors.go

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package access
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/onflow/flow-go/module/irrecoverable"
9+
)
10+
11+
// RequireNoError returns nil if error is nil, otherwise throws an irrecoverable exception
12+
func RequireNoError(ctx context.Context, err error) error {
13+
if err == nil {
14+
return nil
15+
}
16+
17+
irrecoverable.Throw(ctx, err)
18+
return irrecoverable.NewException(err)
19+
}
20+
21+
// RequireErrorIs returns the error if it unwraps to any of the provided target error types
22+
// Otherwise, it throws an irrecoverable exception
23+
func RequireErrorIs(ctx context.Context, err error, targetErrs ...error) error {
24+
if err == nil {
25+
return nil
26+
}
27+
28+
for _, targetErr := range targetErrs {
29+
if errors.Is(err, targetErr) {
30+
return err
31+
}
32+
}
33+
34+
irrecoverable.Throw(ctx, err)
35+
return irrecoverable.NewException(err)
36+
}
37+
38+
// InvalidRequest indicates that the client's request was malformed or invalid
39+
type InvalidRequest struct {
40+
err error
41+
}
42+
43+
func NewInvalidRequest(err error) InvalidRequest {
44+
return InvalidRequest{err: err}
45+
}
46+
47+
func (e InvalidRequest) Error() string {
48+
return fmt.Sprintf("invalid argument: %v", e.err)
49+
}
50+
51+
func (e InvalidRequest) Unwrap() error {
52+
return e.err
53+
}
54+
55+
func IsInvalidRequest(err error) bool {
56+
var errInvalidRequest InvalidRequest
57+
return errors.As(err, &errInvalidRequest)
58+
}
59+
60+
// DataNotFound indicates that the requested data was not found on the system
61+
type DataNotFound struct {
62+
dataType string
63+
err error
64+
}
65+
66+
func NewDataNotFound(dataType string, err error) DataNotFound {
67+
return DataNotFound{dataType: dataType, err: err}
68+
}
69+
70+
func (e DataNotFound) Error() string {
71+
return fmt.Sprintf("data not found for %s: %v", e.dataType, e.err)
72+
}
73+
74+
func (e DataNotFound) Unwrap() error {
75+
return e.err
76+
}
77+
78+
func IsDataNotFound(err error) bool {
79+
var errDataNotFound DataNotFound
80+
return errors.As(err, &errDataNotFound)
81+
}
82+
83+
// InternalError indicates that a non-fatal internal error occurred
84+
// IMPORTANT: this should only be used for benign internal errors. Fatal or irrecoverable system
85+
// errors must be handled explicitly.
86+
type InternalError struct {
87+
err error
88+
}
89+
90+
func NewInternalError(err error) InternalError {
91+
return InternalError{err: err}
92+
}
93+
94+
func (e InternalError) Error() string {
95+
return fmt.Sprintf("internal error: %v", e.err)
96+
}
97+
98+
func (e InternalError) Unwrap() error {
99+
return e.err
100+
}
101+
102+
func IsInternalError(err error) bool {
103+
var errInternalError InternalError
104+
return errors.As(err, &errInternalError)
105+
}
106+
107+
// OutOfRangeError indicates that the request was for data that is outside of the available range.
108+
// This is a more specific version of DataNotFound, where the data is known to eventually exist, but
109+
// currently is not known.
110+
// For example, querying data for a height above the current finalized height.
111+
type OutOfRangeError struct {
112+
err error
113+
}
114+
115+
func NewOutOfRangeError(err error) OutOfRangeError {
116+
return OutOfRangeError{err: err}
117+
}
118+
119+
func (e OutOfRangeError) Error() string {
120+
return fmt.Sprintf("out of range: %v", e.err)
121+
}
122+
123+
func (e OutOfRangeError) Unwrap() error {
124+
return e.err
125+
}
126+
127+
func IsOutOfRangeError(err error) bool {
128+
var errOutOfRangeError OutOfRangeError
129+
return errors.As(err, &errOutOfRangeError)
130+
}
131+
132+
// FailedPrecondition indicates that a precondition for the operation was not met
133+
// This is a more specific version of InvalidRequest, where the request is valid, but the system
134+
// is not currently in a state to fulfill the request (but may be in the future).
135+
type FailedPrecondition struct {
136+
err error
137+
}
138+
139+
func NewFailedPrecondition(err error) FailedPrecondition {
140+
return FailedPrecondition{err: err}
141+
}
142+
143+
func (e FailedPrecondition) Error() string {
144+
return fmt.Sprintf("precondition failed: %v", e.err)
145+
}
146+
147+
func (e FailedPrecondition) Unwrap() error {
148+
return e.err
149+
}
150+
151+
func IsPreconditionFailed(err error) bool {
152+
var errPreconditionFailed FailedPrecondition
153+
return errors.As(err, &errPreconditionFailed)
154+
}

access/errors_test.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package access_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/onflow/flow-go/access"
11+
"github.com/onflow/flow-go/module/irrecoverable"
12+
)
13+
14+
func TestRequireNoError(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("no error", func(t *testing.T) {
18+
t.Parallel()
19+
20+
signalerCtx := irrecoverable.NewMockSignalerContext(t, context.Background())
21+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
22+
23+
err := access.RequireNoError(ctx, nil)
24+
require.NoError(t, err)
25+
})
26+
27+
t.Run("with error", func(t *testing.T) {
28+
t.Parallel()
29+
30+
expectedErr := fmt.Errorf("expected error")
31+
32+
signalerCtx := irrecoverable.NewMockSignalerContextExpectError(t, context.Background(), expectedErr)
33+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
34+
35+
err := access.RequireNoError(ctx, expectedErr)
36+
require.NotErrorIs(t, err, expectedErr, "expected error should be overridden and explicitly not wrapped")
37+
require.Containsf(t, err.Error(), expectedErr.Error(), "expected returned error message to contain original error message")
38+
})
39+
}
40+
41+
func TestRequireErrorIs(t *testing.T) {
42+
t.Parallel()
43+
44+
targetErr := fmt.Errorf("target error")
45+
46+
t.Run("no error", func(t *testing.T) {
47+
t.Parallel()
48+
49+
signalerCtx := irrecoverable.NewMockSignalerContext(t, context.Background())
50+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
51+
52+
err := access.RequireErrorIs(ctx, nil, targetErr)
53+
require.NoError(t, err)
54+
})
55+
56+
t.Run("with expected error", func(t *testing.T) {
57+
t.Parallel()
58+
59+
expectedErr := fmt.Errorf("got err: %w", targetErr)
60+
61+
signalerCtx := irrecoverable.NewMockSignalerContext(t, context.Background())
62+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
63+
64+
err := access.RequireErrorIs(ctx, expectedErr, targetErr)
65+
require.ErrorIs(t, err, expectedErr)
66+
})
67+
68+
t.Run("with multiple expected error", func(t *testing.T) {
69+
t.Parallel()
70+
71+
expectedErr := fmt.Errorf("got err: %w", targetErr)
72+
73+
signalerCtx := irrecoverable.NewMockSignalerContext(t, context.Background())
74+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
75+
76+
err := access.RequireErrorIs(ctx, expectedErr, fmt.Errorf("target error2"), targetErr)
77+
require.ErrorIs(t, err, expectedErr)
78+
})
79+
80+
t.Run("with unexpected error", func(t *testing.T) {
81+
t.Parallel()
82+
83+
expectedErr := fmt.Errorf("expected error")
84+
85+
signalerCtx := irrecoverable.NewMockSignalerContextExpectError(t, context.Background(), expectedErr)
86+
ctx := irrecoverable.WithSignalerContext(context.Background(), signalerCtx)
87+
88+
err := access.RequireErrorIs(ctx, expectedErr, targetErr)
89+
require.NotErrorIs(t, err, expectedErr, "expected error should be overridden and explicitly not wrapped")
90+
require.Containsf(t, err.Error(), expectedErr.Error(), "expected returned error message to contain original error message")
91+
})
92+
}

engine/access/rest/common/error.go

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package common
22

3-
import "net/http"
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
8+
"github.com/onflow/flow-go/access"
9+
)
410

511
// StatusError provides custom error with http status.
612
type StatusError interface {
@@ -56,3 +62,37 @@ func (e *Error) Status() int {
5662
func (e *Error) Error() string {
5763
return e.err.Error()
5864
}
65+
66+
// ErrorToResponseCode converts an Access API error into a grpc status error. The input may either
67+
// be a status.Error already, or an access sentinel error.
68+
func ErrorToResponseCode(err error) StatusError {
69+
if err == nil {
70+
return nil
71+
}
72+
73+
var converted StatusError
74+
if errors.As(err, &converted) {
75+
return converted
76+
}
77+
78+
switch {
79+
case access.IsInvalidRequest(err):
80+
return NewBadRequestError(err)
81+
case access.IsDataNotFound(err):
82+
return NewNotFoundError(err.Error(), err)
83+
case access.IsPreconditionFailed(err):
84+
return NewRestError(http.StatusPreconditionFailed, err.Error(), err)
85+
case access.IsOutOfRangeError(err):
86+
return NewNotFoundError(err.Error(), err)
87+
case access.IsInternalError(err):
88+
return NewRestError(http.StatusInternalServerError, err.Error(), err)
89+
case errors.Is(err, context.Canceled):
90+
return NewRestError(http.StatusRequestTimeout, "Request canceled", err)
91+
case errors.Is(err, context.DeadlineExceeded):
92+
return NewRestError(http.StatusRequestTimeout, "Request deadline exceeded", err)
93+
default:
94+
// TODO: ideally we would throw an exception in this case. For now, report it as Unknown so we
95+
// can more easily identify any missed code paths and fix them while transitioning to this pattern.
96+
return NewRestError(http.StatusInternalServerError, err.Error(), err)
97+
}
98+
}

engine/access/rest/http/routes/execution_result.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func GetExecutionResultsByBlockIDs(r *common.Request, backend access.API, link c
2121
for i, id := range req.BlockIDs {
2222
res, err := backend.GetExecutionResultForBlockID(r.Context(), id)
2323
if err != nil {
24-
return nil, err
24+
return nil, common.ErrorToResponseCode(err)
2525
}
2626

2727
var response commonmodels.ExecutionResult
@@ -44,7 +44,7 @@ func GetExecutionResultByID(r *common.Request, backend access.API, link commonmo
4444

4545
res, err := backend.GetExecutionResultByID(r.Context(), req.ID)
4646
if err != nil {
47-
return nil, err
47+
return nil, common.ErrorToResponseCode(err)
4848
}
4949

5050
if res == nil {

engine/access/rest/http/routes/node_version_info.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func GetNodeVersionInfo(r *common.Request, backend access.API, _ commonmodels.LinkGenerator) (interface{}, error) {
1212
params, err := backend.GetNodeVersionInfo(r.Context())
1313
if err != nil {
14-
return nil, err
14+
return nil, common.ErrorToResponseCode(err)
1515
}
1616

1717
var response models.NodeVersionInfo

0 commit comments

Comments
 (0)