diff --git a/pkg/capabilities/errors/error.go b/pkg/capabilities/errors/error.go new file mode 100644 index 0000000000..99992ea95b --- /dev/null +++ b/pkg/capabilities/errors/error.go @@ -0,0 +1,71 @@ +package errors + +import "fmt" + +type ReportType int + +const ( + // LocalOnly The message in the error may contain sensitive node local information and should not be reported remotely. + // In addition the serialised string representation is prefixed with an identify that prevents the error + // from being accidentally or maliciously marked as remote reportable by manipulating the error string. + LocalOnly ReportType = 0 + + // RemoteReportable The message in the error is safe to report remotely between nodes. + RemoteReportable ReportType = 1 + + // ReportableUser The error is due to user error and is safe to report remotely between nodes. + ReportableUser ReportType = 2 +) + +type Error interface { + error + + ReportType() ReportType + Code() ErrorCode + SerializeToString() string + SerializeToRemoteReportableString() string +} + +type capabilityError struct { + err error + reportType ReportType + errorCode ErrorCode +} + +func newError(err error, reportType ReportType, errorCode ErrorCode) Error { + return &capabilityError{ + err: err, + reportType: reportType, + errorCode: errorCode, + } +} + +// NewRemoteReportableError indicates that the wrapped error does not contain any node local confidential information +// and is safe to report to other nodes in the network. +func NewRemoteReportableError(err error, errorCode ErrorCode) Error { + return newError(err, RemoteReportable, errorCode) +} + +// NewReportableUserError indicates that the wrapped error is due to user error and does not contain any node local confidential information +// and is safe to report to other nodes in the network. +func NewReportableUserError(err error, errorCode ErrorCode) Error { + return newError(err, ReportableUser, errorCode) +} + +// NewLocalReportableError indicates that the wrapped error may contain node local confidential information +// that should not be reported to other nodes in the network. Only the error code and generic message will be reported remotely. +func NewLocalReportableError(err error, errorCode ErrorCode) Error { + return newError(err, LocalOnly, errorCode) +} + +func (e *capabilityError) Error() string { + return fmt.Sprintf("[%d]%s:", e.errorCode, e.errorCode.String()) + " " + e.err.Error() +} + +func (e *capabilityError) ReportType() ReportType { + return e.reportType +} + +func (e *capabilityError) Code() ErrorCode { + return e.errorCode +} diff --git a/pkg/capabilities/errors/error_codes.go b/pkg/capabilities/errors/error_codes.go new file mode 100644 index 0000000000..ffe45f800d --- /dev/null +++ b/pkg/capabilities/errors/error_codes.go @@ -0,0 +1,167 @@ +package errors + +type ErrorCode uint32 + +// *** TODO *** +// Just use GRPC error codes? or use GRPC error codes as inspiration (as below) but customize +// to make them relevant to capabilities?, IMO latter is better as it gives the flexibility to add +// more capability specific error codes (e.g. ConsensusFailed) whilst removing those +// that are not relevant (e.g OK, Unknown, + +const ( + // Uncategorized indicates no appropriate error code available - the capability author should consider adding a + // code that fits better. If the error code does not exist on the receiving side, it will be treated as Uncategorized + // to ensure backwards compatibility. + Uncategorized ErrorCode = 0 + + // Cancelled indicates the operation was canceled (typically by the caller). + Cancelled ErrorCode = 1 + + // InvalidArgument indicates client specified an invalid argument. + // Note that this differs from FailedPrecondition. It indicates arguments + // that are problematic regardless of the state of the system + // (e.g., a malformed file name). + InvalidArgument ErrorCode = 2 + + // DeadlineExceeded means operation expired before completion. + DeadlineExceeded ErrorCode = 3 + + // NotFound means some requested entity (e.g., file or directory) was + // not found. + NotFound ErrorCode = 4 + + // AlreadyExists means an attempt to create an entity failed because one + // already exists. + AlreadyExists ErrorCode = 5 + + // PermissionDenied indicates the caller does not have permission to + // execute the specified operation. It must not be used for rejections + // caused by exhausting some resource (use ResourceExhausted + // instead for those errors). It must not be + // used if the caller cannot be identified (use Unauthenticated + // instead for those errors). + PermissionDenied ErrorCode = 6 + + // ResourceExhausted indicates some resource has been exhausted, perhaps + // a per-user quota, or perhaps the entire file system is out of space. + ResourceExhausted ErrorCode = 7 + + // FailedPrecondition indicates operation was rejected because the + // system is not in a state required for the operation's execution. + // For example, directory to be deleted may be non-empty, an rmdir + // operation is applied to a non-directory, etc. + // + // A litmus test that may help a service implementor in deciding + // between FailedPrecondition, Aborted, and Unavailable: + // (a) Use Unavailable if the client can retry just the failing call. + // (b) Use Aborted if the client should retry at a higher-level + // (e.g., restarting a read-modify-write sequence). + // (c) Use FailedPrecondition if the client should not retry until + // the system state has been explicitly fixed. E.g., if an "rmdir" + // fails because the directory is non-empty, FailedPrecondition + // should be returned since the client should not retry unless + // they have first fixed up the directory by deleting files from it. + // (d) Use FailedPrecondition if the client performs conditional + // REST Get/Update/Delete on a resource and the resource on the + // server does not match the condition. E.g., conflicting + // read-modify-write on the same resource. + FailedPrecondition ErrorCode = 8 + + // Aborted indicates the operation was aborted, typically due to a + // concurrency issue like sequencer check failures, transaction aborts, + // etc. + // + // See litmus test above for deciding between FailedPrecondition, + // Aborted, and Unavailable. + Aborted ErrorCode = 9 + + // OutOfRange means operation was attempted past the valid range. + // E.g., seeking or reading past end of file. + // + // Unlike InvalidArgument, this error indicates a problem that may + // be fixed if the system state changes. For example, a 32-bit file + // system will generate InvalidArgument if asked to read at an + // offset that is not in the range [0,2^32-1], but it will generate + // OutOfRange if asked to read from an offset past the current + // file size. + // + // There is a fair bit of overlap between FailedPrecondition and + // OutOfRange. We recommend using OutOfRange (the more specific + // error) when it applies so that callers who are iterating through + // a space can easily look for an OutOfRange error to detect when + // they are done. + // + // This error code will not be generated by the gRPC framework. + OutOfRange ErrorCode = 10 + + // Unimplemented indicates operation is not implemented or not + // supported/enabled in this service. + // + // This error code will be generated by the gRPC framework. Most + // commonly, you will see this error code when a method implementation + // is missing on the server. It can also be generated for unknown + // compression algorithms or a disagreement as to whether an RPC should + // be streaming. + Unimplemented ErrorCode = 11 + + // Internal errors. Means some invariants expected by underlying + // system has been broken. If you see one of these errors, + // something is very broken. + Internal ErrorCode = 12 + + // Unavailable indicates the service is currently unavailable. + // This is a most likely a transient condition and may be corrected + // by retrying with a backoff. Note that it is not always safe to retry + // non-idempotent operations. + // + // See litmus test above for deciding between FailedPrecondition, + // Aborted, and Unavailable. + Unavailable ErrorCode = 13 + + // DataLoss indicates unrecoverable data loss or corruption. + DataLoss ErrorCode = 14 + + // Unauthenticated indicates the request does not have valid + // authentication credentials for the operation. + Unauthenticated ErrorCode = 15 + + // ConsensusFailed indicates failure to reach consensus + ConsensusFailed ErrorCode = 16 +) + +// String returns the string representation of the ErrorCode. +func (e ErrorCode) String() string { + if s, ok := errorCodeToString[e]; ok { + return s + } + return "Unknown" +} + +var errorCodeToString = map[ErrorCode]string{ + Uncategorized: "Uncategorized", + Cancelled: "Cancelled", + InvalidArgument: "InvalidArgument", + DeadlineExceeded: "DeadlineExceeded", + NotFound: "NotFound", + AlreadyExists: "AlreadyExists", + PermissionDenied: "PermissionDenied", + ResourceExhausted: "ResourceExhausted", + FailedPrecondition: "FailedPrecondition", + Aborted: "Aborted", + OutOfRange: "OutOfRange", + Unimplemented: "Unimplemented", + Internal: "Internal", + Unavailable: "Unavailable", + DataLoss: "DataLoss", + Unauthenticated: "Unauthenticated", + ConsensusFailed: "ConsensusFailed", +} + +// ErrorCodeFromInt returns the ErrorCode for a given int, or Uncategorized if not found. +func ErrorCodeFromInt(i uint32) ErrorCode { + code := ErrorCode(i) + if _, ok := errorCodeToString[code]; ok { + return code + } + return Uncategorized +} diff --git a/pkg/capabilities/errors/error_serialization.go b/pkg/capabilities/errors/error_serialization.go new file mode 100644 index 0000000000..7ed4f39b7d --- /dev/null +++ b/pkg/capabilities/errors/error_serialization.go @@ -0,0 +1,83 @@ +package errors + +import ( + "errors" + "strconv" + "strings" +) + +const remoteReportableErrorIdentifier = "RemoteReportableError:" + +const reportableUserErrorIdentifier = remoteReportableErrorIdentifier + "UserError:" + +const localReportableErrorIdentifier = "LocalReportableError:" + +const errorCodeIdentifier = "ErrorCode=" + +func PrePendLocalReportableErrorIdentifier(errorMessage string) string { + return localReportableErrorIdentifier + errorMessage +} + +// GetErrorCode Returns the error code and removes it from the message if present. +func GetErrorCode(message string) (ErrorCode, string) { + if strings.HasPrefix(message, errorCodeIdentifier) { + rest := message[len(errorCodeIdentifier):] + colonIdx := strings.Index(rest, ":") + if colonIdx != -1 { + codeStr := rest[:colonIdx] + code, err := strconv.ParseUint(codeStr, 10, 32) + if err == nil { + return ErrorCodeFromInt(uint32(code)), rest[colonIdx+1:] + } + } + } + return Uncategorized, message +} + +func DeserializeErrorFromString(errorMsg string) Error { + // Order is important here as reportable user errors also have the remote reportable error identifier. + if strings.HasPrefix(errorMsg, reportableUserErrorIdentifier) { + errorMsg = strings.TrimPrefix(errorMsg, reportableUserErrorIdentifier) + errorCode, msg := GetErrorCode(errorMsg) + return NewReportableUserError(errors.New(msg), errorCode) + } + + if strings.HasPrefix(errorMsg, remoteReportableErrorIdentifier) { + msg := strings.TrimPrefix(errorMsg, remoteReportableErrorIdentifier) + errorCode, msg := GetErrorCode(msg) + return NewRemoteReportableError(errors.New(msg), errorCode) + } + + if strings.HasPrefix(errorMsg, localReportableErrorIdentifier) { + msg := strings.TrimPrefix(errorMsg, localReportableErrorIdentifier) + errorCode, msg := GetErrorCode(msg) + return NewLocalReportableError(errors.New(msg), errorCode) + } + + // Default to local reportable error if no identifier is found. + errorCode, errorMsg := GetErrorCode(errorMsg) + return NewLocalReportableError(errors.New(errorMsg), errorCode) +} + +func (e *capabilityError) SerializeToString() string { + var prefix string + switch e.ReportType() { + case RemoteReportable: + prefix = remoteReportableErrorIdentifier + case ReportableUser: + prefix = reportableUserErrorIdentifier + case LocalOnly: + prefix = localReportableErrorIdentifier + } + + return prefix + errorCodeIdentifier + strconv.Itoa(int(e.Code())) + ":" + e.err.Error() +} + +func (e *capabilityError) SerializeToRemoteReportableString() string { + switch e.ReportType() { + case RemoteReportable, ReportableUser: + return e.SerializeToString() + } + + return localReportableErrorIdentifier + errorCodeIdentifier + strconv.Itoa(int(e.Code())) + ": failed to execute capability - error message is not remotely reportable" +} diff --git a/pkg/capabilities/errors/error_serialization_test.go b/pkg/capabilities/errors/error_serialization_test.go new file mode 100644 index 0000000000..c12e7fe1b8 --- /dev/null +++ b/pkg/capabilities/errors/error_serialization_test.go @@ -0,0 +1,196 @@ +package errors_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" +) + +func Test_DeserializeFromString(t *testing.T) { + testDeserialization := func(t *testing.T, serialisedRepresentation, expectedError string, expectedCode caperrors.ErrorCode, expectedReportType caperrors.ReportType) { + err := caperrors.DeserializeErrorFromString(serialisedRepresentation) + require.Equal(t, expectedError, err.Error()) + require.Equal(t, expectedCode, err.Code()) + require.Equal(t, expectedReportType, err.ReportType()) + } + + // Remote reportable errors + testDeserialization(t, + "RemoteReportableError:"+"some remote reportable error occurred", + "[0]Uncategorized: some remote reportable error occurred", + caperrors.Uncategorized, + caperrors.RemoteReportable, + ) + + testDeserialization(t, + "RemoteReportableError:ErrorCode=3:"+"some remote reportable error occurred", + "[3]DeadlineExceeded: some remote reportable error occurred", + caperrors.DeadlineExceeded, + caperrors.RemoteReportable, + ) + + testDeserialization(t, + "RemoteReportableError:ErrorCode=45:"+"some remote reportable error occurred", + "[0]Uncategorized: some remote reportable error occurred", + caperrors.Uncategorized, + caperrors.RemoteReportable, + ) + + // User reportable errors + testDeserialization(t, + "RemoteReportableError:UserError:"+"some user reportable error occurred", + "[0]Uncategorized: some user reportable error occurred", + caperrors.Uncategorized, + caperrors.ReportableUser, + ) + + testDeserialization(t, + "RemoteReportableError:UserError:ErrorCode=4:"+"some user reportable error occurred", + "[4]NotFound: some user reportable error occurred", + caperrors.NotFound, + caperrors.ReportableUser, + ) + + testDeserialization(t, + "RemoteReportableError:UserError:ErrorCode=50:"+"some user reportable error occurred", + "[0]Uncategorized: some user reportable error occurred", + caperrors.Uncategorized, + caperrors.ReportableUser, + ) + + // Local reportable errors + testDeserialization(t, + "LocalReportableError:"+"some local reportable error occurred", + "[0]Uncategorized: some local reportable error occurred", + caperrors.Uncategorized, + caperrors.LocalOnly, + ) + + testDeserialization(t, + "LocalReportableError:ErrorCode=5:"+"some local reportable error occurred", + "[5]AlreadyExists: some local reportable error occurred", + caperrors.AlreadyExists, + caperrors.LocalOnly, + ) + + testDeserialization(t, + "LocalReportableError:ErrorCode=-4:"+"some local reportable error occurred", + "[0]Uncategorized: some local reportable error occurred", + caperrors.Uncategorized, + caperrors.LocalOnly, + ) + + // No identifier error - to ensure backwards compatibility with older versions that do not use the reporting type identifiers + testDeserialization(t, + "failed to execute capability", + "[0]Uncategorized: failed to execute capability", + caperrors.Uncategorized, + caperrors.LocalOnly, + ) +} + +func Test_SerializeToString(t *testing.T) { + serializeAndAssert := func(t *testing.T, err caperrors.Error, expectedSerializedForm string) { + serialized := err.SerializeToString() + require.Equal(t, expectedSerializedForm, serialized) + } + + serializeAndAssert(t, + caperrors.NewRemoteReportableError( + errors.New("some remote reportable error occurred"), + caperrors.DeadlineExceeded, + ), + "RemoteReportableError:ErrorCode=3:some remote reportable error occurred", + ) + + serializeAndAssert(t, + caperrors.NewReportableUserError( + errors.New("some user reportable error occurred"), + caperrors.NotFound, + ), + "RemoteReportableError:UserError:ErrorCode=4:some user reportable error occurred", + ) + + serializeAndAssert(t, + caperrors.NewLocalReportableError( + errors.New("some local reportable error occurred"), + caperrors.AlreadyExists, + ), + "LocalReportableError:ErrorCode=5:some local reportable error occurred", + ) +} + +func Test_SerializeToRemoteReportableString(t *testing.T) { + serializeToRemoteReportableStringAndAssert := func(t *testing.T, err caperrors.Error, expectedSerializedForm string) { + serialized := err.SerializeToRemoteReportableString() + require.Equal(t, expectedSerializedForm, serialized) + } + + // Remote reportable error + remoteReportableError := caperrors.NewRemoteReportableError( + errors.New("some remote reportable error occurred"), + caperrors.DeadlineExceeded, + ) + serializeToRemoteReportableStringAndAssert(t, remoteReportableError, "RemoteReportableError:ErrorCode=3:some remote reportable error occurred") + + // User reportable error + userReportableError := caperrors.NewReportableUserError( + errors.New("some user reportable error occurred"), + caperrors.NotFound, + ) + serializeToRemoteReportableStringAndAssert(t, userReportableError, "RemoteReportableError:UserError:ErrorCode=4:some user reportable error occurred") + + // Local reportable error + localReportableError := caperrors.NewLocalReportableError( + errors.New("some local reportable error occurred"), + caperrors.AlreadyExists, + ) + serializeToRemoteReportableStringAndAssert(t, localReportableError, "LocalReportableError:ErrorCode=5: failed to execute capability - error message is not remotely reportable") +} + +// Legacy format used before ReportableUser, LocalOnly types and Error Codes were introduced +func Test_DeserializeFromLegacyRemoteReportableString(t *testing.T) { + assertDeserialization(t, + fmt.Errorf("failed to execute capability: %w", errors.New("some error occurred")).Error(), + "[0]Uncategorized: failed to execute capability: some error occurred", + caperrors.Uncategorized, + caperrors.LocalOnly, + ) +} + +func assertDeserialization(t *testing.T, serialized string, expectedError string, expectedCode caperrors.ErrorCode, expectedReportType caperrors.ReportType) { + err := caperrors.DeserializeErrorFromString(serialized) + require.Equal(t, err.Error(), expectedError) + require.Equal(t, err.Code(), expectedCode) + require.Equal(t, err.ReportType(), expectedReportType) +} + +func Test_DeserializeFromRemoteReportableString(t *testing.T) { + // Remote reportable error + assertDeserialization(t, + "RemoteReportableError: failed to execute capability", + "[0]Uncategorized: failed to execute capability", + caperrors.Uncategorized, + caperrors.RemoteReportable, + ) + + // User reportable error + assertDeserialization(t, + "RemoteReportableError:UserError: failed to execute capability", + "[0]Uncategorized: failed to execute capability", + caperrors.Uncategorized, + caperrors.ReportableUser, + ) + + // Local reportable error + assertDeserialization(t, + "LocalReportableError:ErrorCode=5: failed to execute capability", + "[5]AlreadyExists: failed to execute capability", + caperrors.AlreadyExists, + caperrors.LocalOnly, + ) +} diff --git a/pkg/capabilities/remote_reportable_error.go b/pkg/capabilities/remote_reportable_error.go deleted file mode 100644 index b128c399e0..0000000000 --- a/pkg/capabilities/remote_reportable_error.go +++ /dev/null @@ -1,45 +0,0 @@ -package capabilities - -import ( - "fmt" - "strings" -) - -const remoteReportableErrorIdentifier = "RemoteReportableError:" - -// RemoteReportableError wraps an error to indicate that the error does contain any node specific -// information and is safe to report remotely between nodes. -type RemoteReportableError struct { - err error -} - -func NewRemoteReportableError(err error) *RemoteReportableError { - return &RemoteReportableError{err: err} -} - -func (e *RemoteReportableError) Error() string { - if e.err != nil { - return fmt.Sprintf("%v", e.err) - } - return "" -} - -// Unwrap allows errors.Is and errors.As to work with CustomError. -func (e *RemoteReportableError) Unwrap() error { - return e.err -} - -func PrePendRemoteReportableErrorIdentifier(errorMessage string) string { - return remoteReportableErrorIdentifier + errorMessage -} - -func IsRemoteReportableErrorMessage(message string) bool { - return strings.HasPrefix(message, remoteReportableErrorIdentifier) -} - -func RemoveRemoteReportableErrorIdentifier(message string) string { - if IsRemoteReportableErrorMessage(message) { - return strings.TrimPrefix(message, remoteReportableErrorIdentifier) - } - return message -} diff --git a/pkg/capabilities/remote_reportable_error_test.go b/pkg/capabilities/remote_reportable_error_test.go deleted file mode 100644 index f4f6e0ec41..0000000000 --- a/pkg/capabilities/remote_reportable_error_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package capabilities - -import ( - "errors" - "testing" -) - -// TestReportableError_As checks if errors.As works with RemoteReportableError. -func TestReportableError_As(t *testing.T) { - underlyingErr := errors.New("underlying error") - reportableErr := NewRemoteReportableError(underlyingErr) - - var target *RemoteReportableError - if !errors.As(reportableErr, &target) { - t.Fatalf("expected errors.As to identify RemoteReportableError") - } -} - -// TestReportableError_Message checks if the underlying error's message is correctly output. -func TestReportableError_Message(t *testing.T) { - underlyingErr := errors.New("underlying error message") - reportableErr := NewRemoteReportableError(underlyingErr) - - if reportableErr.Error() != "underlying error message" { - t.Fatalf("expected error message to be %q, got %q", "underlying error message", reportableErr.Error()) - } -} diff --git a/pkg/capabilities/remote_unreportable_error.go b/pkg/capabilities/remote_unreportable_error.go deleted file mode 100644 index 4514d8be5b..0000000000 --- a/pkg/capabilities/remote_unreportable_error.go +++ /dev/null @@ -1,15 +0,0 @@ -package capabilities - -import ( - "strings" -) - -const remoteUnreportableErrorIdentifier = "RemoteUnreportableError:" - -func PrePendRemoteUnreportableErrorIdentifier(errorMessage string) string { - return remoteUnreportableErrorIdentifier + errorMessage -} - -func RemoveRemoteUnreportableErrorIdentifier(message string) string { - return strings.TrimPrefix(message, remoteUnreportableErrorIdentifier) -} diff --git a/pkg/capabilities/reportable_user_error.go b/pkg/capabilities/reportable_user_error.go deleted file mode 100644 index afaf5361fc..0000000000 --- a/pkg/capabilities/reportable_user_error.go +++ /dev/null @@ -1,43 +0,0 @@ -package capabilities - -import ( - "strings" -) - -const reportableUserErrorIdentifier = remoteReportableErrorIdentifier + "UserError:" - -// ReportableUserError indicates the error is a user error that is both locally and remotely reportable. -type ReportableUserError struct { - err *RemoteReportableError -} - -func NewReportableUserError(err error) *ReportableUserError { - return &ReportableUserError{err: &RemoteReportableError{err}} -} - -func (e *ReportableUserError) Error() string { - if e.err == nil { - return "" - } - - return e.err.Error() -} - -func (e *ReportableUserError) Unwrap() error { - return e.err -} - -func PrePendReportableUserErrorIdentifier(errorMessage string) string { - return reportableUserErrorIdentifier + errorMessage -} - -func IsReportableUserErrorMessage(message string) bool { - return strings.HasPrefix(message, reportableUserErrorIdentifier) -} - -func RemoveReportableUserErrorIdentifier(message string) string { - if IsReportableUserErrorMessage(message) { - return strings.TrimPrefix(message, reportableUserErrorIdentifier) - } - return message -} diff --git a/pkg/capabilities/reportable_user_error_test.go b/pkg/capabilities/reportable_user_error_test.go deleted file mode 100644 index 6e70f6bd92..0000000000 --- a/pkg/capabilities/reportable_user_error_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package capabilities - -import ( - "errors" - "testing" -) - -// TestReportableUserError_As checks if errors.As works with ReportableUserError. -func TestReportableUserError_As(t *testing.T) { - underlyingErr := NewRemoteReportableError(errors.New("underlying error")) - userErr := NewReportableUserError(underlyingErr) - - var target *ReportableUserError - if !errors.As(userErr, &target) { - t.Fatalf("expected errors.As to identify ReportableUserError") - } -} - -// TestReportableUserError_Message checks if the underlying error's message is correctly output. -func TestReportableUserError_Message(t *testing.T) { - underlyingErr := errors.New("underlying user error message") - userErr := NewReportableUserError(underlyingErr) - - if userErr.Error() != "underlying user error message" { - t.Fatalf("expected error message to be %q, got %q", "underlying user error message", userErr.Error()) - } -} - -func TestAsRemoteReportableErrorForRemoteReportableUserError(t *testing.T) { - underlyingErr := errors.New("underlying user error message") - userErr := NewReportableUserError(underlyingErr) - - var target *ReportableUserError - if !errors.As(userErr, &target) { - t.Fatalf("expected errors.As to identify ReportableUserError") - } - - var targetReportableError *RemoteReportableError - if !errors.As(userErr, &targetReportableError) { - t.Fatalf("expected errors.As to identify RemoteReportableError from ReportableUserError") - } -} diff --git a/pkg/capabilities/v2/consensus/consensus.pb.go b/pkg/capabilities/v2/consensus/consensus.pb.go index 6dcd509568..36883f13b8 100644 --- a/pkg/capabilities/v2/consensus/consensus.pb.go +++ b/pkg/capabilities/v2/consensus/consensus.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.8 +// protoc-gen-go v1.36.7 // protoc v5.29.3 // source: capabilities/internal/consensus/v1alpha/consensus.proto diff --git a/pkg/capabilities/v2/consensus/server/consensus_server_gen.go b/pkg/capabilities/v2/consensus/server/consensus_server_gen.go index 81d122d1fa..8fd928cf46 100644 --- a/pkg/capabilities/v2/consensus/server/consensus_server_gen.go +++ b/pkg/capabilities/v2/consensus/server/consensus_server_gen.go @@ -12,6 +12,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) @@ -19,9 +20,9 @@ import ( var _ = emptypb.Empty{} type ConsensusCapability interface { - Simple(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.SimpleConsensusInputs) (*capabilities.ResponseAndMetadata[*pb.Value], error) + Simple(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.SimpleConsensusInputs) (*capabilities.ResponseAndMetadata[*pb.Value], caperrors.Error) - Report(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.ReportRequest) (*capabilities.ResponseAndMetadata[*sdk.ReportResponse], error) + Report(ctx context.Context, metadata capabilities.RequestMetadata, input *sdk.ReportRequest) (*capabilities.ResponseAndMetadata[*sdk.ReportResponse], caperrors.Error) Start(ctx context.Context) error Close() error diff --git a/pkg/capabilities/v2/protoc/pkg/templates/server.go.tmpl b/pkg/capabilities/v2/protoc/pkg/templates/server.go.tmpl index 02b6b35aed..6b1c6ba9f1 100644 --- a/pkg/capabilities/v2/protoc/pkg/templates/server.go.tmpl +++ b/pkg/capabilities/v2/protoc/pkg/templates/server.go.tmpl @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/core" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/loop" ) @@ -39,7 +40,7 @@ type {{.GoName}}Capability interface { Unregister{{.GoName}}(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *{{ImportAlias .Input.GoIdent.GoImportPath}}.{{.Input.GoIdent.GoName}}) error {{- else }} {{ $hasActions = true }} - {{.GoName}}(ctx context.Context, metadata capabilities.RequestMetadata, input *{{ImportAlias .Input.GoIdent.GoImportPath}}.{{.Input.GoIdent.GoName}} {{if ne "emptypb.Empty" (ConfigType $service)}}, {{(ConfigType $service)}}{{ end }}) (*capabilities.ResponseAndMetadata[*{{ImportAlias .Output.GoIdent.GoImportPath}}.{{.Output.GoIdent.GoName}}], error) + {{.GoName}}(ctx context.Context, metadata capabilities.RequestMetadata, input *{{ImportAlias .Input.GoIdent.GoImportPath}}.{{.Input.GoIdent.GoName}} {{if ne "emptypb.Empty" (ConfigType $service)}}, {{(ConfigType $service)}}{{ end }}) (*capabilities.ResponseAndMetadata[*{{ImportAlias .Output.GoIdent.GoImportPath}}.{{.Output.GoIdent.GoName}}], caperrors.Error) {{- end }} {{- end }} diff --git a/pkg/loop/internal/core/services/capability/capabilities.go b/pkg/loop/internal/core/services/capability/capabilities.go index d4b51651d0..645a3bb546 100644 --- a/pkg/loop/internal/core/services/capability/capabilities.go +++ b/pkg/loop/internal/core/services/capability/capabilities.go @@ -12,6 +12,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/net" @@ -421,17 +422,13 @@ func (c *executableServer) Execute(reqpb *capabilitiespb.CapabilityRequest, serv var responseMessage *capabilitiespb.CapabilityResponse response, err := c.impl.Execute(server.Context(), req) if err != nil { - var reportableError *capabilities.RemoteReportableError - var userError *capabilities.ReportableUserError - // The order is important here, as ReportableUserError is a subtype of RemoteReportableError - if errors.As(err, &userError) { - responseMessage = &capabilitiespb.CapabilityResponse{Error: capabilities.PrePendReportableUserErrorIdentifier(err.Error())} - } else if errors.As(err, &reportableError) { - responseMessage = &capabilitiespb.CapabilityResponse{Error: capabilities.PrePendRemoteReportableErrorIdentifier(err.Error())} + var capabilityError caperrors.Error + if errors.As(err, &capabilityError) { + responseMessage = &capabilitiespb.CapabilityResponse{Error: capabilityError.SerializeToString()} } else { - // All other errors are treated as remote unreportable and are marked as such to prevent accidental or malicious + // All other errors are treated as local reportable only and are marked as such to prevent accidental or malicious // reporting of sensitive information by prefixing the error message with the remote reportable identifier. - responseMessage = &capabilitiespb.CapabilityResponse{Error: capabilities.PrePendRemoteUnreportableErrorIdentifier(err.Error())} + responseMessage = &capabilitiespb.CapabilityResponse{Error: caperrors.PrePendLocalReportableErrorIdentifier(err.Error())} } } else { responseMessage = pb.CapabilityResponseToProto(response) @@ -470,22 +467,7 @@ func (c *executableClient) Execute(ctx context.Context, req capabilities.Capabil } if resp.Error != "" { - // The order is important here, as ReportableUserError is a subtype of RemoteReportableError - if capabilities.IsReportableUserErrorMessage(resp.Error) { - return capabilities.CapabilityResponse{}, capabilities.NewReportableUserError( - errors.New(capabilities.RemoveReportableUserErrorIdentifier(resp.Error))) - } - - if capabilities.IsRemoteReportableErrorMessage(resp.Error) { - return capabilities.CapabilityResponse{}, capabilities.NewRemoteReportableError( - errors.New(capabilities.RemoveRemoteReportableErrorIdentifier(resp.Error))) - } - - // The error message may or make not have been prepended with the unreportable error identifier depending on - // if the capability is running locally or remotely. In either case, remove the remote unreportable identifier if it exists. - removedIdentifierErrorMessage := capabilities.RemoveRemoteUnreportableErrorIdentifier(resp.Error) - - return capabilities.CapabilityResponse{}, errors.New(removedIdentifierErrorMessage) + return capabilities.CapabilityResponse{}, caperrors.DeserializeErrorFromString(resp.Error) } r, err := pb.CapabilityResponseFromProto(resp) diff --git a/pkg/loop/internal/core/services/capability/capabilities_test.go b/pkg/loop/internal/core/services/capability/capabilities_test.go index d6324dae28..ea8bf2442a 100644 --- a/pkg/loop/internal/core/services/capability/capabilities_test.go +++ b/pkg/loop/internal/core/services/capability/capabilities_test.go @@ -15,6 +15,7 @@ import ( "google.golang.org/grpc/status" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/loop/internal/net" "github.com/smartcontractkit/chainlink-protos/cre/go/values" @@ -458,7 +459,7 @@ func Test_Capabilities(t *testing.T) { assert.Equal(t, expectedResp, resp) }) - t.Run("fetching an action capability, and executing it with error", func(t *testing.T) { + t.Run("fetching an action capability, and executing it with reportable error", func(t *testing.T) { ma := mustMockExecutable(t, capabilities.CapabilityTypeAction) c, _, _, err := newCapabilityPlugin(t, ma) require.NoError(t, err) @@ -473,16 +474,20 @@ func Test_Capabilities(t *testing.T) { Inputs: imap, } - ma.responseError = errors.New("bang") + ma.responseError = caperrors.NewRemoteReportableError(errors.New("bang"), caperrors.DeadlineExceeded) - _, err = c.(capabilities.ExecutableCapability).Execute( + _, err = c.(capabilities.ActionCapability).Execute( t.Context(), expectedRequest) require.Error(t, err) - assert.Equal(t, "bang", err.Error()) + capErr := err.(caperrors.Error) + + require.Equal(t, "[3]DeadlineExceeded: bang", capErr.Error()) + require.Equal(t, caperrors.DeadlineExceeded, capErr.Code()) + require.Equal(t, caperrors.RemoteReportable, capErr.ReportType()) }) - t.Run("fetching an action capability, and executing it with reportable error", func(t *testing.T) { + t.Run("fetching an action capability, and executing it with reportable user error", func(t *testing.T) { ma := mustMockExecutable(t, capabilities.CapabilityTypeAction) c, _, _, err := newCapabilityPlugin(t, ma) require.NoError(t, err) @@ -497,19 +502,20 @@ func Test_Capabilities(t *testing.T) { Inputs: imap, } - ma.responseError = capabilities.NewRemoteReportableError(errors.New("bang")) + ma.responseError = caperrors.NewReportableUserError(errors.New("bang"), caperrors.NotFound) _, err = c.(capabilities.ActionCapability).Execute( t.Context(), expectedRequest) require.Error(t, err) - assert.Equal(t, "bang", err.Error()) + capErr := err.(caperrors.Error) - var reportableError *capabilities.RemoteReportableError - assert.ErrorAs(t, err, &reportableError) + require.Equal(t, "[4]NotFound: bang", capErr.Error()) + require.Equal(t, caperrors.NotFound, capErr.Code()) + require.Equal(t, caperrors.ReportableUser, capErr.ReportType()) }) - t.Run("fetching an action capability, and executing it with reportable user error", func(t *testing.T) { + t.Run("fetching an action capability, and executing it with local error", func(t *testing.T) { ma := mustMockExecutable(t, capabilities.CapabilityTypeAction) c, _, _, err := newCapabilityPlugin(t, ma) require.NoError(t, err) @@ -524,16 +530,46 @@ func Test_Capabilities(t *testing.T) { Inputs: imap, } - ma.responseError = capabilities.NewReportableUserError(errors.New("bang")) + ma.responseError = caperrors.NewLocalReportableError(errors.New("bang"), caperrors.DeadlineExceeded) + + _, err = c.(capabilities.ActionCapability).Execute( + t.Context(), + expectedRequest) + require.Error(t, err) + capErr := err.(caperrors.Error) + + require.Equal(t, "[3]DeadlineExceeded: bang", capErr.Error()) + require.Equal(t, caperrors.DeadlineExceeded, capErr.Code()) + require.Equal(t, caperrors.LocalOnly, capErr.ReportType()) + }) + + // This will only happen a local capability has not had it's API migrated to always return capability.Error + t.Run("fetching an action capability, and executing it without capability error", func(t *testing.T) { + ma := mustMockExecutable(t, capabilities.CapabilityTypeAction) + c, _, _, err := newCapabilityPlugin(t, ma) + require.NoError(t, err) + + cmap, err := values.NewMap(map[string]any{"foo": "bar"}) + require.NoError(t, err) + + imap, err := values.NewMap(map[string]any{"bar": "baz"}) + require.NoError(t, err) + expectedRequest := capabilities.CapabilityRequest{ + Config: cmap, + Inputs: imap, + } + + ma.responseError = errors.New("bang") _, err = c.(capabilities.ActionCapability).Execute( t.Context(), expectedRequest) require.Error(t, err) - assert.Equal(t, "bang", err.Error()) + capErr := err.(caperrors.Error) - var reportableUserError *capabilities.ReportableUserError - assert.ErrorAs(t, err, &reportableUserError) + require.Equal(t, "[0]Uncategorized: bang", capErr.Error()) + require.Equal(t, caperrors.Uncategorized, capErr.Code()) + require.Equal(t, caperrors.LocalOnly, capErr.ReportType()) }) t.Run("fetching an action capability, and closing it", func(t *testing.T) {