From 2d340cb60952f3f96494b66f757d38bb1f39be06 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Mon, 13 Oct 2025 13:16:51 +0100 Subject: [PATCH 1/2] :sparkles: `[errors]` Added a way to provide context (`DescribeCircumstance`) to an error but without changing is type if it is a common error --- changes/20251013123506.feature | 1 + utils/commonerrors/errors.go | 117 +++++++++++++++++++++++++++++- utils/commonerrors/errors_test.go | 62 ++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 changes/20251013123506.feature diff --git a/changes/20251013123506.feature b/changes/20251013123506.feature new file mode 100644 index 0000000000..34f0a0ef60 --- /dev/null +++ b/changes/20251013123506.feature @@ -0,0 +1 @@ +:sparkles: `[errors]` Added a way to provide context (`DescribeCircumstance`) to an error but without changing is type if it is a common error diff --git a/utils/commonerrors/errors.go b/utils/commonerrors/errors.go index beee3f27f7..e94c61780b 100644 --- a/utils/commonerrors/errors.go +++ b/utils/commonerrors/errors.go @@ -73,6 +73,89 @@ func IsCommonError(target error) bool { return Any(target, ErrNotImplemented, ErrNoExtension, ErrNoLogger, ErrNoLoggerSource, ErrNoLogSource, ErrUndefined, ErrInvalidDestination, ErrTimeout, ErrLocked, ErrStaleLock, ErrExists, ErrNotFound, ErrUnsupported, ErrUnavailable, ErrWrongUser, ErrUnauthorised, ErrUnknown, ErrInvalid, ErrConflict, ErrMarshalling, ErrCancelled, ErrEmpty, ErrUnexpected, ErrTooLarge, ErrForbidden, ErrCondition, ErrEOF, ErrMalicious, ErrWarning, ErrOutOfRange, ErrFailed) } +// RetrieveCommonError tries to return the common error of an error. +func RetrieveCommonError(target error) (isCommonError bool, commonError error) { + switch { + case IsEmpty(target): + return true, nil + case Any(target, ErrNotImplemented): + return true, ErrNotImplemented + case Any(target, ErrNoExtension): + return true, ErrNoExtension + case Any(target, ErrNoLogger): + return true, ErrNoLogger + case Any(target, ErrNoLoggerSource): + return true, ErrNoLoggerSource + case Any(target, ErrNoLogSource): + return true, ErrNoLogSource + case Any(target, ErrUndefined): + return true, ErrUndefined + case Any(target, ErrInvalidDestination): + return true, ErrInvalidDestination + case Any(target, ErrTimeout): + return true, ErrTimeout + case Any(target, ErrLocked): + return true, ErrLocked + case Any(target, ErrStaleLock): + return true, ErrStaleLock + case Any(target, ErrExists): + return true, ErrExists + case Any(target, ErrNotFound): + return true, ErrNotFound + case Any(target, ErrUnsupported): + return true, ErrUnsupported + case Any(target, ErrUnavailable): + return true, ErrUnavailable + case Any(target, ErrWrongUser): + return true, ErrWrongUser + case Any(target, ErrUnauthorised): + return true, ErrUnauthorised + case Any(target, ErrUnknown): + return true, ErrUnknown + case Any(target, ErrInvalid): + return true, ErrInvalid + case Any(target, ErrConflict): + return true, ErrConflict + case Any(target, ErrMarshalling): + return true, ErrMarshalling + case Any(target, ErrCancelled): + return true, ErrCancelled + case Any(target, ErrEmpty): + return true, ErrEmpty + case Any(target, ErrUnexpected): + return true, ErrUnexpected + case Any(target, ErrTooLarge): + return true, ErrTooLarge + case Any(target, ErrForbidden): + return true, ErrForbidden + case Any(target, ErrCondition): + return true, ErrCondition + case Any(target, ErrEOF): + return true, ErrEOF + case Any(target, ErrMalicious): + return true, ErrMalicious + case Any(target, ErrOutOfRange): + return true, ErrOutOfRange + case Any(target, ErrFailed): + return true, ErrFailed + case Any(target, ErrWarning): + return true, ErrWarning + } + + underlyingErr, parseError := GetUnderlyingErrorType(target) + if parseError != nil { + commonError = ErrUnknown + return + } + if IsCommonError(underlyingErr) { + commonError = underlyingErr + isCommonError = true + } else { + commonError = ErrUnknown + } + return +} + // Any determines whether the target error is of the same type as any of the errors `err` func Any(target error, err ...error) bool { for i := range err { @@ -371,6 +454,36 @@ func Errorf(targetErr error, format string, args ...any) error { } } +// DescribeCircumstance adds some context to a particular error. If the error is of a known type (as in, a common error), it will be kept. Otherwise, it will be set as Unexpected. +func DescribeCircumstance(originalError error, circumstance string) error { + origErr := ConvertContextError(originalError) + isCommonError, commonError := RetrieveCommonError(origErr) + if isCommonError { + return WrapError(commonError, origErr, circumstance) + } + return WrapError(ErrUnexpected, origErr, circumstance) +} + +// DescribeCircumstanceAndKeepType does almost the same as DescribeCircumstance but if the err is not a common error, it won't wrap it but just add the context string to it. +func DescribeCircumstanceAndKeepType(err error, circumstance string) error { + origErr := ConvertContextError(err) + isCommonError, commonError := RetrieveCommonError(origErr) + if isCommonError { + return WrapError(commonError, origErr, circumstance) + } + return New(origErr, circumstance) +} + +// DescribeCircumstanceAndKeepTypef is like DescribeCircumstanceAndKeepType but uses a message format to describe the error context. +func DescribeCircumstanceAndKeepTypef(err error, circumstanceFormat string, args ...any) error { + return DescribeCircumstanceAndKeepType(err, fmt.Sprintf(circumstanceFormat, args...)) +} + +// DescribeCircumstancef is the same as DescribeCircumstance but uses a format for capturing the error context. +func DescribeCircumstancef(err error, circumstanceFormat string, args ...any) error { + return DescribeCircumstance(err, fmt.Sprintf(circumstanceFormat, args...)) +} + // WrapError wraps an error into a particular targetError. However, if the original error has to do with a contextual error (i.e. ErrCancelled or ErrTimeout) or should be considered as a failure rather than an error, it will be passed through without having its type changed. // Same is true with warnings. // This method should be used to safely wrap errors without losing information about context control information. @@ -407,7 +520,7 @@ func WrapIfNotCommonError(targetError, originalError error, msg string) error { return WrapError(targetError, originalError, msg) } if IsCommonError(originalError) { - return New(originalError, msg) + return DescribeCircumstance(originalError, msg) } return WrapError(targetError, originalError, msg) } @@ -426,7 +539,7 @@ func WrapIfNotCommonErrorf(targetError, originalError error, msgFormat string, a return WrapErrorf(targetError, originalError, msgFormat, args...) } if IsCommonError(originalError) { - return Newf(originalError, msgFormat, args...) + return DescribeCircumstancef(originalError, msgFormat, args...) } return WrapErrorf(targetError, originalError, msgFormat, args...) } diff --git a/utils/commonerrors/errors_test.go b/utils/commonerrors/errors_test.go index e0d231269a..2a8c210b28 100644 --- a/utils/commonerrors/errors_test.go +++ b/utils/commonerrors/errors_test.go @@ -127,11 +127,44 @@ func TestIsCommonError(t *testing.T) { } for i := range commonErrors { assert.True(t, IsCommonError(commonErrors[i])) + is, rawError := RetrieveCommonError(commonErrors[i]) + assert.True(t, is) + assert.ErrorIs(t, rawError, commonErrors[i]) + assert.True(t, Any(rawError, commonErrors[i])) } assert.False(t, IsCommonError(errors.New(faker.Sentence()))) } +func TestRetrieveCommonError(t *testing.T) { + is, rawError := RetrieveCommonError(nil) + assert.True(t, is) + assert.True(t, Any(rawError, nil)) + + is, rawError = RetrieveCommonError(errors.New(" ")) + assert.True(t, is) + assert.True(t, Any(rawError, nil)) + + is, rawError = RetrieveCommonError(ErrUndefined) + assert.True(t, is) + assert.ErrorIs(t, rawError, ErrUndefined) + assert.True(t, Any(rawError, ErrUndefined)) + + is, rawError = RetrieveCommonError(fmt.Errorf("%w: %v", ErrInvalid, faker.Sentence())) + assert.True(t, is) + assert.ErrorIs(t, rawError, ErrInvalid) + assert.True(t, Any(rawError, ErrInvalid)) + + is, rawError = RetrieveCommonError(fmt.Errorf("%v: %v", ErrCondition.Error(), faker.Sentence())) + assert.True(t, is) + assert.ErrorIs(t, rawError, ErrCondition) + assert.True(t, Any(rawError, ErrCondition)) + + is, rawError = RetrieveCommonError(errors.New(faker.Sentence())) + assert.False(t, is) + assert.ErrorIs(t, rawError, ErrUnknown) +} + func TestIsWarning(t *testing.T) { assert.True(t, IsWarning(ErrWarning)) assert.True(t, IsWarning(NewWarningMessage(faker.Sentence()))) @@ -256,6 +289,35 @@ func TestWrapError(t *testing.T) { assert.True(t, Any(WrapIfNotCommonErrorf(ErrUndefined, errors.New(faker.Sentence()), faker.Sentence()), ErrUndefined)) } +func TestDescribeCircumstance(t *testing.T) { + tmpErr := errors.New(faker.Name()) + assert.False(t, IsCommonError(tmpErr)) + assert.True(t, Any(DescribeCircumstance(context.Canceled, faker.Sentence()), ErrCancelled)) + assert.True(t, Any(DescribeCircumstance(ErrConflict, faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstance(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstance(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstance(tmpErr, faker.Sentence()), ErrUnexpected)) + assert.Equal(t, "conflict: some context: conflict: initial error", DescribeCircumstance(New(ErrConflict, "initial error"), "some context").Error()) + assert.True(t, Any(DescribeCircumstancef(context.Canceled, "%v", faker.Sentence()), ErrCancelled)) + assert.True(t, Any(DescribeCircumstancef(ErrConflict, "%v", faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstancef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstancef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstancef(tmpErr, "%v", faker.Sentence()), ErrUnexpected)) + assert.True(t, Any(DescribeCircumstanceAndKeepType(context.Canceled, faker.Sentence()), ErrCancelled)) + assert.True(t, Any(DescribeCircumstanceAndKeepType(ErrConflict, faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstanceAndKeepType(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstanceAndKeepType(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict)) + assert.False(t, Any(DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), ErrUnexpected)) + assert.ErrorIs(t, DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), tmpErr) + assert.True(t, Any(DescribeCircumstanceAndKeepTypef(context.Canceled, "%v", faker.Sentence()), ErrCancelled)) + assert.True(t, Any(DescribeCircumstanceAndKeepTypef(ErrConflict, "%v", faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstanceAndKeepTypef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict)) + assert.True(t, Any(DescribeCircumstanceAndKeepTypef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict)) + assert.False(t, Any(DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), ErrUnexpected)) + assert.ErrorIs(t, DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), tmpErr) + +} + func TestString(t *testing.T) { assert.Equal(t, "unknown", New(nil, "").Error()) assert.Equal(t, "unknown", Newf(nil, "").Error()) From a88f6227d3a5f0debc89d204060b6b449e434ab1 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Mon, 13 Oct 2025 14:05:50 +0100 Subject: [PATCH 2/2] Update utils/commonerrors/errors.go Co-authored-by: Kem Govender --- utils/commonerrors/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/commonerrors/errors.go b/utils/commonerrors/errors.go index e94c61780b..933f2f0520 100644 --- a/utils/commonerrors/errors.go +++ b/utils/commonerrors/errors.go @@ -464,7 +464,7 @@ func DescribeCircumstance(originalError error, circumstance string) error { return WrapError(ErrUnexpected, origErr, circumstance) } -// DescribeCircumstanceAndKeepType does almost the same as DescribeCircumstance but if the err is not a common error, it won't wrap it but just add the context string to it. +// DescribeCircumstanceAndKeepType does almost the same as DescribeCircumstance but if the error is not a common error, it won't wrap it but just add the context string to it. func DescribeCircumstanceAndKeepType(err error, circumstance string) error { origErr := ConvertContextError(err) isCommonError, commonError := RetrieveCommonError(origErr)