Skip to content

Commit

Permalink
Port the connect error categorization
Browse files Browse the repository at this point in the history
  • Loading branch information
chriso committed Jun 9, 2024
1 parent dd43359 commit eb14d72
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 0 deletions.
43 changes: 43 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"reflect"
"strings"

"connectrpc.com/connect"
"golang.org/x/sys/unix"
)

Expand Down Expand Up @@ -148,6 +149,9 @@ func errorStatus(err error, depth int) Status {
case *tls.RecordHeaderError:
return TLSErrorStatus

case *connect.Error:
return connectErrorStatus(e)

case status:
return e.Status()

Expand Down Expand Up @@ -211,6 +215,45 @@ func errnoStatus(errno unix.Errno) Status {
}
}

func connectErrorStatus(err *connect.Error) Status {
switch err.Code() {
case connect.CodeCanceled: // 408 Request Timeout
return TimeoutStatus
case connect.CodeUnknown: // 500 Internal Server Error
return TemporaryErrorStatus
case connect.CodeInvalidArgument: // 400 Bad Request
return InvalidArgumentStatus
case connect.CodeDeadlineExceeded: // 408 Request Timeout
return TimeoutStatus
case connect.CodeNotFound: // 404 Not Found
return NotFoundStatus
case connect.CodeAlreadyExists: // 409 Conflict
return PermanentErrorStatus
case connect.CodePermissionDenied: // 403 Forbidden
return PermissionDeniedStatus
case connect.CodeResourceExhausted: // 429 Too Many Requests
return ThrottledStatus
case connect.CodeFailedPrecondition: // 412 Precondition Failed
return PermanentErrorStatus
case connect.CodeAborted: // 409 Conflict
return PermanentErrorStatus
case connect.CodeOutOfRange: // 400 Bad Request
return InvalidArgumentStatus
case connect.CodeUnimplemented: // 404 Not Found
return NotFoundStatus
case connect.CodeInternal: // 500 Internal Server Error
return TemporaryErrorStatus
case connect.CodeUnavailable: // 503 Service Unavailable
return TemporaryErrorStatus
case connect.CodeDataLoss: // 500 Internal Server Error
return PermanentErrorStatus
case connect.CodeUnauthenticated: // 401 Unauthorized
return UnauthenticatedStatus
default:
return PermanentErrorStatus
}
}

func isIOError(err error) bool {
switch err {
case io.EOF,
Expand Down
142 changes: 142 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"testing"
"time"

"connectrpc.com/connect"
"github.com/dispatchrun/dispatch-go"
)

Expand Down Expand Up @@ -703,6 +704,147 @@ func TestErrorStatus(t *testing.T) {
status: dispatch.InvalidResponseStatus,
},

// The SDK uses the connect library when remotely interacting with functions.
// Connect uses gRPC error codes. Check that the correct Dispatch status is
// derived from these error codes.

{
scenario: "connect.CodeCanceled",
error: func(*testing.T) error {
return connect.NewError(connect.CodeCanceled, errors.New("the request was canceled"))
},
status: dispatch.TimeoutStatus,
},

{
scenario: "connect.CodeUnknown",
error: func(*testing.T) error {
return connect.NewError(connect.CodeUnknown, errors.New("unknown"))
},
status: dispatch.TemporaryErrorStatus,
},

{
scenario: "connect.CodeInvalidArgument",
error: func(*testing.T) error {
underlying := connect.NewError(connect.CodeInvalidArgument, errors.New("invalid argument"))
return fmt.Errorf("something went wrong: %w", underlying)
},
status: dispatch.InvalidArgumentStatus,
},

{
scenario: "connect.CodeDeadlineExceeded",
error: func(*testing.T) error {
return connect.NewError(connect.CodeDeadlineExceeded, errors.New("deadline exceeded"))
},
status: dispatch.TimeoutStatus,
},

{
scenario: "connect.CodeNotFound",
error: func(*testing.T) error {
return connect.NewError(connect.CodeNotFound, errors.New("not found"))
},
status: dispatch.NotFoundStatus,
},

{
scenario: "connect.CodeAlreadyExists",
error: func(*testing.T) error {
return connect.NewError(connect.CodeAlreadyExists, errors.New("already exists"))
},
status: dispatch.PermanentErrorStatus,
},

{
scenario: "connect.CodePermissionDenied",
error: func(*testing.T) error {
return connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
},
status: dispatch.PermissionDeniedStatus,
},

{
scenario: "connect.CodeResourceExhausted",
error: func(*testing.T) error {
return connect.NewError(connect.CodeResourceExhausted, errors.New("resource exhausted"))
},
status: dispatch.ThrottledStatus,
},

{
scenario: "connect.CodeFailedPrecondition",
error: func(*testing.T) error {
return connect.NewError(connect.CodeFailedPrecondition, errors.New("failed precondition"))
},
status: dispatch.PermanentErrorStatus,
},

{
scenario: "connect.CodeAborted",
error: func(*testing.T) error {
return connect.NewError(connect.CodeAborted, errors.New("aborted"))
},
status: dispatch.PermanentErrorStatus,
},

{
scenario: "connect.CodeOutOfRange",
error: func(*testing.T) error {
return connect.NewError(connect.CodeOutOfRange, errors.New("out of range"))
},
status: dispatch.InvalidArgumentStatus,
},

{
scenario: "connect.CodeUnimplemented",
error: func(*testing.T) error {
return connect.NewError(connect.CodeUnimplemented, errors.New("unimplemented"))
},
status: dispatch.NotFoundStatus,
},

{
scenario: "connect.CodeInternal",
error: func(*testing.T) error {
return connect.NewError(connect.CodeInternal, errors.New("internal"))
},
status: dispatch.TemporaryErrorStatus,
},

{
scenario: "connect.CodeUnavailable",
error: func(*testing.T) error {
return connect.NewError(connect.CodeUnavailable, errors.New("unavailable"))
},
status: dispatch.TemporaryErrorStatus,
},

{
scenario: "connect.CodeDataLoss",
error: func(*testing.T) error {
return connect.NewError(connect.CodeDataLoss, errors.New("data loss"))
},
status: dispatch.PermanentErrorStatus,
},

{
scenario: "connect.CodeUnauthenticated",
error: func(*testing.T) error {
return connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
},
status: dispatch.UnauthenticatedStatus,
},

{
scenario: "connect.CodeUnauthenticated",
error: func(*testing.T) error {
return connect.NewError(connect.Code(9999), errors.New("unknown"))
},
status: dispatch.PermanentErrorStatus,
},

// The default behavior is to assume permanent errors, but we still want
// to validate that a few common cases are handled as expected.
//
Expand Down

0 comments on commit eb14d72

Please sign in to comment.