Skip to content

Commit 9f87502

Browse files
committed
Add a resolve error function to return first error
When an error object is returned and must be resolved to a single return error, the first error matching one defined by this package should be returned. Signed-off-by: Derek McGowan <[email protected]>
1 parent 038bb7b commit 9f87502

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

resolve.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package errdefs
18+
19+
import "context"
20+
21+
// Resolve returns the first error found in the error chain which matches an
22+
// error defined in this package or context error. A raw, unwrapped error is
23+
// returned or ErrUnknown if no matching error is found.
24+
//
25+
// This is useful for determining a response code based on the outermost wrapped
26+
// error rather than the original cause. For example, a not found error deep
27+
// in the code may be wrapped as an invalid argument. When determining status
28+
// code from Is* functions, the depth or ordering of the error is not
29+
// considered.
30+
//
31+
// The search order is depth first, a wrapped error returned from any part of
32+
// the chain from `Unwrap() error` will be returned before any joined errors
33+
// as returned by `Unwrap() []error`.
34+
func Resolve(err error) error {
35+
if err == nil {
36+
return nil
37+
}
38+
err = firstError(err)
39+
if err == nil {
40+
err = ErrUnknown
41+
}
42+
return err
43+
}
44+
45+
func firstError(err error) error {
46+
for {
47+
switch err {
48+
case ErrUnknown,
49+
ErrInvalidArgument,
50+
ErrNotFound,
51+
ErrAlreadyExists,
52+
ErrPermissionDenied,
53+
ErrResourceExhausted,
54+
ErrFailedPrecondition,
55+
ErrConflict,
56+
ErrNotModified,
57+
ErrAborted,
58+
ErrOutOfRange,
59+
ErrNotImplemented,
60+
ErrInternal,
61+
ErrUnavailable,
62+
ErrDataLoss,
63+
ErrUnauthenticated,
64+
context.DeadlineExceeded,
65+
context.Canceled:
66+
return err
67+
}
68+
switch e := err.(type) {
69+
case unknown:
70+
return ErrUnknown
71+
case invalidParameter:
72+
return ErrInvalidArgument
73+
case notFound:
74+
return ErrNotFound
75+
// Skip ErrAlreadyExists, no interface defined
76+
case forbidden:
77+
return ErrPermissionDenied
78+
// Skip ErrResourceExhasuted, no interface defined
79+
// Skip ErrFailedPrecondition, no interface defined
80+
case conflict:
81+
return ErrConflict
82+
case notModified:
83+
return ErrNotModified
84+
// Skip ErrAborted, no interface defined
85+
// Skip ErrOutOfRange, no interface defined
86+
case notImplemented:
87+
return ErrNotImplemented
88+
case system:
89+
return ErrInternal
90+
case unavailable:
91+
return ErrUnavailable
92+
case dataLoss:
93+
return ErrDataLoss
94+
case unauthorized:
95+
return ErrUnauthenticated
96+
case deadlineExceeded:
97+
return context.DeadlineExceeded
98+
case cancelled:
99+
return context.Canceled
100+
case interface{ Unwrap() error }:
101+
err = e.Unwrap()
102+
if err == nil {
103+
return nil
104+
}
105+
case interface{ Unwrap() []error }:
106+
for _, ue := range e.Unwrap() {
107+
if fe := firstError(ue); fe != nil {
108+
return fe
109+
}
110+
}
111+
return nil
112+
default:
113+
return nil
114+
}
115+
}
116+
}

resolve_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package errdefs
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
"testing"
24+
)
25+
26+
func TestResolve(t *testing.T) {
27+
for i, tc := range []struct {
28+
err error
29+
resolved error
30+
}{
31+
{nil, nil},
32+
{wrap(ErrUnknown), ErrUnknown},
33+
{wrap(ErrNotFound), ErrNotFound},
34+
{wrap(ErrInvalidArgument), ErrInvalidArgument},
35+
{wrap(ErrNotFound), ErrNotFound},
36+
{wrap(ErrAlreadyExists), ErrAlreadyExists},
37+
{wrap(ErrPermissionDenied), ErrPermissionDenied},
38+
{wrap(ErrResourceExhausted), ErrResourceExhausted},
39+
{wrap(ErrFailedPrecondition), ErrFailedPrecondition},
40+
{wrap(ErrConflict), ErrConflict},
41+
{wrap(ErrNotModified), ErrNotModified},
42+
{wrap(ErrAborted), ErrAborted},
43+
{wrap(ErrOutOfRange), ErrOutOfRange},
44+
{wrap(ErrNotImplemented), ErrNotImplemented},
45+
{wrap(ErrInternal), ErrInternal},
46+
{wrap(ErrUnavailable), ErrUnavailable},
47+
{wrap(ErrDataLoss), ErrDataLoss},
48+
{wrap(ErrUnauthenticated), ErrUnauthenticated},
49+
{wrap(context.DeadlineExceeded), context.DeadlineExceeded},
50+
{wrap(context.Canceled), context.Canceled},
51+
{errors.Join(errors.New("untyped"), wrap(ErrInvalidArgument)), ErrInvalidArgument},
52+
{errors.Join(ErrConflict, ErrNotFound), ErrConflict},
53+
{errors.New("untyped"), ErrUnknown},
54+
{errors.Join(wrap(ErrUnauthenticated), ErrNotModified), ErrUnauthenticated},
55+
{ErrDataLoss, ErrDataLoss},
56+
{errors.Join(ErrOutOfRange), ErrOutOfRange},
57+
{errors.Join(ErrNotImplemented, ErrInternal), ErrNotImplemented},
58+
{context.Canceled, context.Canceled},
59+
{testUnavailable{}, ErrUnavailable},
60+
{wrap(testUnavailable{}), ErrUnavailable},
61+
{errors.Join(testUnavailable{}, ErrPermissionDenied), ErrUnavailable},
62+
{errors.Join(errors.New("untyped join")), ErrUnknown},
63+
{errors.Join(errors.New("untyped1"), errors.New("untyped2")), ErrUnknown},
64+
} {
65+
name := fmt.Sprintf("%d-%s", i, errorString(tc.resolved))
66+
tc := tc
67+
t.Run(name, func(t *testing.T) {
68+
resolved := Resolve(tc.err)
69+
if resolved != tc.resolved {
70+
t.Errorf("Expected %s, got %s", tc.resolved, resolved)
71+
}
72+
})
73+
}
74+
}
75+
76+
func wrap(err error) error {
77+
err = fmt.Errorf("wrapped error: %w", err)
78+
return fmt.Errorf("%w and also %w", err, ErrUnknown)
79+
}
80+
81+
func errorString(err error) string {
82+
if err == nil {
83+
return "nil"
84+
}
85+
return err.Error()
86+
}
87+
88+
type testUnavailable struct{}
89+
90+
func (testUnavailable) Error() string { return "" }
91+
func (testUnavailable) Unavailable() {}

0 commit comments

Comments
 (0)