Skip to content

Commit 1c624c9

Browse files
authored
feat: add error wrapping support to ResolutionError (#438)
* feat: add error wrapping support to ResolutionError Add the ability to preserve original errors in ResolutionError. This enables providers to wrap underlying errors while maintaining backward compatibility, allowing callers to use errors.Is() and errors.As() for error inspection. Signed-off-by: Roman Dmytrenko <[email protected]> * address PR feedback Signed-off-by: Roman Dmytrenko <[email protected]> * address PR feedback Signed-off-by: Roman Dmytrenko <[email protected]> --------- Signed-off-by: Roman Dmytrenko <[email protected]>
1 parent 40fe27e commit 1c624c9

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

openfeature/resolution_error.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,22 @@ const (
3030
type ResolutionError struct {
3131
// fields are unexported, this means providers are forced to create structs of this type using one of the constructors below.
3232
// this effectively emulates an enum
33-
code ErrorCode
34-
message string
33+
code ErrorCode
34+
message string
35+
originalErr error
3536
}
3637

38+
// Error implements the error interface for ResolutionError.
3739
func (r ResolutionError) Error() string {
40+
// Avoid including original error message to prevent leaking internal details externally.
3841
return fmt.Sprintf("%s: %s", r.code, r.message)
3942
}
4043

44+
// Unwrap allows access to the original error, if any.
45+
func (r ResolutionError) Unwrap() error {
46+
return r.originalErr
47+
}
48+
4149
// NewProviderNotReadyResolutionError constructs a resolution error with code PROVIDER_NOT_READY
4250
//
4351
// Explanation - The value was resolved before the provider was ready.
@@ -61,11 +69,8 @@ func NewFlagNotFoundResolutionError(msg string) ResolutionError {
6169
// NewParseErrorResolutionError constructs a resolution error with code PARSE_ERROR
6270
//
6371
// Explanation - An error was encountered parsing data, such as a flag configuration.
64-
func NewParseErrorResolutionError(msg string) ResolutionError {
65-
return ResolutionError{
66-
code: ParseErrorCode,
67-
message: msg,
68-
}
72+
func NewParseErrorResolutionError(msg string, errs ...error) ResolutionError {
73+
return newResolutionError(ParseErrorCode, msg, errs...)
6974
}
7075

7176
// NewTypeMismatchResolutionError constructs a resolution error with code TYPE_MISMATCH
@@ -101,10 +106,25 @@ func NewInvalidContextResolutionError(msg string) ResolutionError {
101106
// NewGeneralResolutionError constructs a resolution error with code GENERAL
102107
//
103108
// Explanation - The error was for a reason not enumerated above.
104-
func NewGeneralResolutionError(msg string) ResolutionError {
109+
func NewGeneralResolutionError(msg string, errs ...error) ResolutionError {
110+
return newResolutionError(GeneralCode, msg, errs...)
111+
}
112+
113+
// newResolutionError is a helper to create a ResolutionError with an optional original error.
114+
func newResolutionError(code ErrorCode, msg string, errs ...error) ResolutionError {
115+
var originalErr error
116+
switch len(errs) {
117+
case 0:
118+
originalErr = nil // being explicit
119+
case 1:
120+
originalErr = errs[0]
121+
default:
122+
originalErr = errors.Join(errs...)
123+
}
105124
return ResolutionError{
106-
code: GeneralCode,
107-
message: msg,
125+
code: code,
126+
message: msg,
127+
originalErr: originalErr,
108128
}
109129
}
110130

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package openfeature
2+
3+
import (
4+
"errors"
5+
"io"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestResolutionErrorWithOriginal(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
err error
14+
}{
15+
{"general", NewGeneralResolutionError("flag not found", io.ErrNoProgress)},
16+
{"parse", NewParseErrorResolutionError("flag not found", io.ErrNoProgress)},
17+
}
18+
19+
for _, tt := range tests {
20+
t.Run(tt.name, func(t *testing.T) {
21+
err := tt.err
22+
t.Run("wraps original error", func(t *testing.T) {
23+
if !errors.Is(err, io.ErrNoProgress) {
24+
t.Errorf("expected error to wrap %v", io.ErrNoProgress)
25+
}
26+
})
27+
28+
t.Run("does not match unrelated error", func(t *testing.T) {
29+
if errors.Is(err, io.EOF) {
30+
t.Errorf("expected error to not match %v", io.EOF)
31+
}
32+
})
33+
34+
t.Run("contains expected message", func(t *testing.T) {
35+
if !strings.Contains(err.Error(), "flag not found") {
36+
t.Errorf("expected message to contain %q, got %q", "flag not found", err.Error())
37+
}
38+
})
39+
40+
t.Run("unwrap returns original", func(t *testing.T) {
41+
if unwrapped := errors.Unwrap(err); unwrapped != io.ErrNoProgress {
42+
t.Errorf("expected unwrap to return %v, got %v", io.ErrNoProgress, unwrapped)
43+
}
44+
})
45+
})
46+
}
47+
}

0 commit comments

Comments
 (0)