From b478333ec361b41504132543ee68553f287ddbfa Mon Sep 17 00:00:00 2001 From: Matthew Pendrey Date: Tue, 12 May 2026 10:11:19 +0100 Subject: [PATCH 1/4] mark errors as limit error --- pkg/settings/limits/errors.go | 24 +++++++++++++ pkg/settings/limits/errors_test.go | 55 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/pkg/settings/limits/errors.go b/pkg/settings/limits/errors.go index 93600a6355..e2fce4628b 100644 --- a/pkg/settings/limits/errors.go +++ b/pkg/settings/limits/errors.go @@ -11,6 +11,16 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/settings" ) +// LimitError marks errors returned when a settings limit applies. +// Use errors.As to detect any limit error, for example: +// +// var le LimitError +// if errors.As(err, &le) { ... } +type LimitError interface { + error + isLimitError() +} + type ErrorRateLimited struct { Key string @@ -22,6 +32,8 @@ type ErrorRateLimited struct { Err error } +func (ErrorRateLimited) isLimitError() {} + func (e ErrorRateLimited) Unwrap() error { return e.Err } func (e ErrorRateLimited) GRPCStatus() *status.Status { @@ -51,6 +63,8 @@ type ErrorResourceLimited[N Number] struct { Used, Limit, Amount N } +func (ErrorResourceLimited[N]) isLimitError() {} + func (e ErrorResourceLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -74,6 +88,8 @@ type ErrorTimeLimited struct { Timeout time.Duration } +func (ErrorTimeLimited) isLimitError() {} + func (e ErrorTimeLimited) GRPCStatus() *status.Status { return status.New(codes.DeadlineExceeded, e.Error()) } @@ -107,6 +123,8 @@ type ErrorBoundLimited[N Number] struct { Limit, Amount N } +func (ErrorBoundLimited[N]) isLimitError() {} + func (e ErrorBoundLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -131,6 +149,8 @@ type ErrorRangeLimited[N Number] struct { Amount N } +func (ErrorRangeLimited[N]) isLimitError() {} + func (e ErrorRangeLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -154,6 +174,8 @@ type ErrorQueueFull struct { Limit int } +func (ErrorQueueFull) isLimitError() {} + func (e ErrorQueueFull) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -177,6 +199,8 @@ type ErrorNotAllowed struct { Tenant string } +func (ErrorNotAllowed) isLimitError() {} + func (e ErrorNotAllowed) GRPCStatus() *status.Status { return status.New(codes.PermissionDenied, e.Error()) } diff --git a/pkg/settings/limits/errors_test.go b/pkg/settings/limits/errors_test.go index e139e340c4..30382d2931 100644 --- a/pkg/settings/limits/errors_test.go +++ b/pkg/settings/limits/errors_test.go @@ -2,6 +2,7 @@ package limits import ( "errors" + "fmt" "testing" "time" @@ -229,3 +230,57 @@ func marshalUnmarshalError(t *testing.T, err error) error { require.NoError(t, proto.Unmarshal(b, &pb)) return status.FromProto(&pb).Err() } + +func TestLimitError_errorsAs(t *testing.T) { + for _, tt := range []struct { + name string + err error + }{ + { + name: "rate", + err: ErrorRateLimited{Err: errors.New("inner")}, + }, + { + name: "resource", + err: ErrorResourceLimited[int]{Limit: 1, Used: 0, Amount: 1}, + }, + { + name: "time", + err: ErrorTimeLimited{Timeout: time.Second}, + }, + { + name: "bound", + err: ErrorBoundLimited[int]{Limit: 1, Amount: 2}, + }, + { + name: "range", + err: ErrorRangeLimited[int]{ + Limit: settings.Range[int]{Lower: 0, Upper: 1}, + Amount: 2, + }, + }, + { + name: "queue_full", + err: ErrorQueueFull{Limit: 1}, + }, + { + name: "not_allowed", + err: ErrorNotAllowed{}, + }, + { + name: "wrapped", + err: fmt.Errorf("wrap: %w", ErrorTimeLimited{Timeout: time.Second}), + }, + } { + t.Run(tt.name, func(t *testing.T) { + var le LimitError + require.True(t, errors.As(tt.err, &le), "expected limit error") + }) + } +} + +func TestLimitError_errorsAs_negative(t *testing.T) { + var le LimitError + assert.False(t, errors.As(ErrQueueEmpty, &le)) + assert.False(t, errors.As(errors.New("other"), &le)) +} From 2212e649e475e447b4e7047de727e21f0ebf7f24 Mon Sep 17 00:00:00 2001 From: Matthew Pendrey Date: Tue, 12 May 2026 17:01:27 +0100 Subject: [PATCH 2/4] additional constructor for limit breach errors --- pkg/capabilities/errors/error.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/capabilities/errors/error.go b/pkg/capabilities/errors/error.go index 86f4d00b48..73c58cf0c7 100644 --- a/pkg/capabilities/errors/error.go +++ b/pkg/capabilities/errors/error.go @@ -1,6 +1,10 @@ package errors -import "fmt" +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" +) type Origin int @@ -110,6 +114,12 @@ func NewPublicUserError(err error, errorCode ErrorCode) Error { return NewError(err, VisibilityPublic, OriginUser, errorCode) } +// NewLimitBreachedError creates a public user error that embeds the provided limit error, indicating that a CRE limit +// breach has occurred. The error message will include the provided errorMsg as a prefix for additional context. +func NewLimitBreachedError(errorMsg string, err limits.LimitError) Error { + return NewPublicUserError(fmt.Errorf("%s: %w", errorMsg, err), LimitExceeded) +} + // NewPrivateSystemError indicates that the wrapped error is due to a system-level issue and may contain // sensitive information that should only be visible to the node on which it occurred. The error code will still be // visible to other nodes in the network. From 3fcfb2162d6dfcac1f5cdbaf6d2f252f3d9273fc Mon Sep 17 00:00:00 2001 From: Matthew Pendrey Date: Tue, 12 May 2026 17:20:24 +0100 Subject: [PATCH 3/4] move to regular error --- pkg/capabilities/errors/error.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/capabilities/errors/error.go b/pkg/capabilities/errors/error.go index 73c58cf0c7..ed92a11960 100644 --- a/pkg/capabilities/errors/error.go +++ b/pkg/capabilities/errors/error.go @@ -2,8 +2,6 @@ package errors import ( "fmt" - - "github.com/smartcontractkit/chainlink-common/pkg/settings/limits" ) type Origin int @@ -116,7 +114,7 @@ func NewPublicUserError(err error, errorCode ErrorCode) Error { // NewLimitBreachedError creates a public user error that embeds the provided limit error, indicating that a CRE limit // breach has occurred. The error message will include the provided errorMsg as a prefix for additional context. -func NewLimitBreachedError(errorMsg string, err limits.LimitError) Error { +func NewLimitBreachedError(errorMsg string, err error) Error { return NewPublicUserError(fmt.Errorf("%s: %w", errorMsg, err), LimitExceeded) } From 196a6657dd477b798113dfdc4819f947be25d8d2 Mon Sep 17 00:00:00 2001 From: Matthew Pendrey Date: Tue, 12 May 2026 17:58:11 +0100 Subject: [PATCH 4/4] remove marker interface --- pkg/settings/limits/errors.go | 24 ------------- pkg/settings/limits/errors_test.go | 55 ------------------------------ 2 files changed, 79 deletions(-) diff --git a/pkg/settings/limits/errors.go b/pkg/settings/limits/errors.go index e2fce4628b..93600a6355 100644 --- a/pkg/settings/limits/errors.go +++ b/pkg/settings/limits/errors.go @@ -11,16 +11,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/settings" ) -// LimitError marks errors returned when a settings limit applies. -// Use errors.As to detect any limit error, for example: -// -// var le LimitError -// if errors.As(err, &le) { ... } -type LimitError interface { - error - isLimitError() -} - type ErrorRateLimited struct { Key string @@ -32,8 +22,6 @@ type ErrorRateLimited struct { Err error } -func (ErrorRateLimited) isLimitError() {} - func (e ErrorRateLimited) Unwrap() error { return e.Err } func (e ErrorRateLimited) GRPCStatus() *status.Status { @@ -63,8 +51,6 @@ type ErrorResourceLimited[N Number] struct { Used, Limit, Amount N } -func (ErrorResourceLimited[N]) isLimitError() {} - func (e ErrorResourceLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -88,8 +74,6 @@ type ErrorTimeLimited struct { Timeout time.Duration } -func (ErrorTimeLimited) isLimitError() {} - func (e ErrorTimeLimited) GRPCStatus() *status.Status { return status.New(codes.DeadlineExceeded, e.Error()) } @@ -123,8 +107,6 @@ type ErrorBoundLimited[N Number] struct { Limit, Amount N } -func (ErrorBoundLimited[N]) isLimitError() {} - func (e ErrorBoundLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -149,8 +131,6 @@ type ErrorRangeLimited[N Number] struct { Amount N } -func (ErrorRangeLimited[N]) isLimitError() {} - func (e ErrorRangeLimited[N]) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -174,8 +154,6 @@ type ErrorQueueFull struct { Limit int } -func (ErrorQueueFull) isLimitError() {} - func (e ErrorQueueFull) GRPCStatus() *status.Status { return status.New(codes.ResourceExhausted, e.Error()) } @@ -199,8 +177,6 @@ type ErrorNotAllowed struct { Tenant string } -func (ErrorNotAllowed) isLimitError() {} - func (e ErrorNotAllowed) GRPCStatus() *status.Status { return status.New(codes.PermissionDenied, e.Error()) } diff --git a/pkg/settings/limits/errors_test.go b/pkg/settings/limits/errors_test.go index 30382d2931..e139e340c4 100644 --- a/pkg/settings/limits/errors_test.go +++ b/pkg/settings/limits/errors_test.go @@ -2,7 +2,6 @@ package limits import ( "errors" - "fmt" "testing" "time" @@ -230,57 +229,3 @@ func marshalUnmarshalError(t *testing.T, err error) error { require.NoError(t, proto.Unmarshal(b, &pb)) return status.FromProto(&pb).Err() } - -func TestLimitError_errorsAs(t *testing.T) { - for _, tt := range []struct { - name string - err error - }{ - { - name: "rate", - err: ErrorRateLimited{Err: errors.New("inner")}, - }, - { - name: "resource", - err: ErrorResourceLimited[int]{Limit: 1, Used: 0, Amount: 1}, - }, - { - name: "time", - err: ErrorTimeLimited{Timeout: time.Second}, - }, - { - name: "bound", - err: ErrorBoundLimited[int]{Limit: 1, Amount: 2}, - }, - { - name: "range", - err: ErrorRangeLimited[int]{ - Limit: settings.Range[int]{Lower: 0, Upper: 1}, - Amount: 2, - }, - }, - { - name: "queue_full", - err: ErrorQueueFull{Limit: 1}, - }, - { - name: "not_allowed", - err: ErrorNotAllowed{}, - }, - { - name: "wrapped", - err: fmt.Errorf("wrap: %w", ErrorTimeLimited{Timeout: time.Second}), - }, - } { - t.Run(tt.name, func(t *testing.T) { - var le LimitError - require.True(t, errors.As(tt.err, &le), "expected limit error") - }) - } -} - -func TestLimitError_errorsAs_negative(t *testing.T) { - var le LimitError - assert.False(t, errors.As(ErrQueueEmpty, &le)) - assert.False(t, errors.As(errors.New("other"), &le)) -}