Apierrors Is<<ErrType>>: Support wrapped errors

This commit is contained in:
Alvaro Aleman 2020-02-27 19:08:50 +01:00
parent 650220fa64
commit 3244350046
2 changed files with 302 additions and 30 deletions

View File

@ -18,6 +18,7 @@ package errors
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@ -483,127 +484,141 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr
} }
// IsNotFound returns true if the specified error was created by NewNotFound. // IsNotFound returns true if the specified error was created by NewNotFound.
// It supports wrapped errors.
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return ReasonForError(err) == metav1.StatusReasonNotFound return ReasonForError(err) == metav1.StatusReasonNotFound
} }
// IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists. // IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists.
// It supports wrapped errors.
func IsAlreadyExists(err error) bool { func IsAlreadyExists(err error) bool {
return ReasonForError(err) == metav1.StatusReasonAlreadyExists return ReasonForError(err) == metav1.StatusReasonAlreadyExists
} }
// IsConflict determines if the err is an error which indicates the provided update conflicts. // IsConflict determines if the err is an error which indicates the provided update conflicts.
// It supports wrapped errors.
func IsConflict(err error) bool { func IsConflict(err error) bool {
return ReasonForError(err) == metav1.StatusReasonConflict return ReasonForError(err) == metav1.StatusReasonConflict
} }
// IsInvalid determines if the err is an error which indicates the provided resource is not valid. // IsInvalid determines if the err is an error which indicates the provided resource is not valid.
// It supports wrapped errors.
func IsInvalid(err error) bool { func IsInvalid(err error) bool {
return ReasonForError(err) == metav1.StatusReasonInvalid return ReasonForError(err) == metav1.StatusReasonInvalid
} }
// IsGone is true if the error indicates the requested resource is no longer available. // IsGone is true if the error indicates the requested resource is no longer available.
// It supports wrapped errors.
func IsGone(err error) bool { func IsGone(err error) bool {
return ReasonForError(err) == metav1.StatusReasonGone return ReasonForError(err) == metav1.StatusReasonGone
} }
// IsResourceExpired is true if the error indicates the resource has expired and the current action is // IsResourceExpired is true if the error indicates the resource has expired and the current action is
// no longer possible. // no longer possible.
// It supports wrapped errors.
func IsResourceExpired(err error) bool { func IsResourceExpired(err error) bool {
return ReasonForError(err) == metav1.StatusReasonExpired return ReasonForError(err) == metav1.StatusReasonExpired
} }
// IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header // IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header
// It supports wrapped errors.
func IsNotAcceptable(err error) bool { func IsNotAcceptable(err error) bool {
return ReasonForError(err) == metav1.StatusReasonNotAcceptable return ReasonForError(err) == metav1.StatusReasonNotAcceptable
} }
// IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header // IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header
// It supports wrapped errors.
func IsUnsupportedMediaType(err error) bool { func IsUnsupportedMediaType(err error) bool {
return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType
} }
// IsMethodNotSupported determines if the err is an error which indicates the provided action could not // IsMethodNotSupported determines if the err is an error which indicates the provided action could not
// be performed because it is not supported by the server. // be performed because it is not supported by the server.
// It supports wrapped errors.
func IsMethodNotSupported(err error) bool { func IsMethodNotSupported(err error) bool {
return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed
} }
// IsServiceUnavailable is true if the error indicates the underlying service is no longer available. // IsServiceUnavailable is true if the error indicates the underlying service is no longer available.
// It supports wrapped errors.
func IsServiceUnavailable(err error) bool { func IsServiceUnavailable(err error) bool {
return ReasonForError(err) == metav1.StatusReasonServiceUnavailable return ReasonForError(err) == metav1.StatusReasonServiceUnavailable
} }
// IsBadRequest determines if err is an error which indicates that the request is invalid. // IsBadRequest determines if err is an error which indicates that the request is invalid.
// It supports wrapped errors.
func IsBadRequest(err error) bool { func IsBadRequest(err error) bool {
return ReasonForError(err) == metav1.StatusReasonBadRequest return ReasonForError(err) == metav1.StatusReasonBadRequest
} }
// IsUnauthorized determines if err is an error which indicates that the request is unauthorized and // IsUnauthorized determines if err is an error which indicates that the request is unauthorized and
// requires authentication by the user. // requires authentication by the user.
// It supports wrapped errors.
func IsUnauthorized(err error) bool { func IsUnauthorized(err error) bool {
return ReasonForError(err) == metav1.StatusReasonUnauthorized return ReasonForError(err) == metav1.StatusReasonUnauthorized
} }
// IsForbidden determines if err is an error which indicates that the request is forbidden and cannot // IsForbidden determines if err is an error which indicates that the request is forbidden and cannot
// be completed as requested. // be completed as requested.
// It supports wrapped errors.
func IsForbidden(err error) bool { func IsForbidden(err error) bool {
return ReasonForError(err) == metav1.StatusReasonForbidden return ReasonForError(err) == metav1.StatusReasonForbidden
} }
// IsTimeout determines if err is an error which indicates that request times out due to long // IsTimeout determines if err is an error which indicates that request times out due to long
// processing. // processing.
// It supports wrapped errors.
func IsTimeout(err error) bool { func IsTimeout(err error) bool {
return ReasonForError(err) == metav1.StatusReasonTimeout return ReasonForError(err) == metav1.StatusReasonTimeout
} }
// IsServerTimeout determines if err is an error which indicates that the request needs to be retried // IsServerTimeout determines if err is an error which indicates that the request needs to be retried
// by the client. // by the client.
// It supports wrapped errors.
func IsServerTimeout(err error) bool { func IsServerTimeout(err error) bool {
return ReasonForError(err) == metav1.StatusReasonServerTimeout return ReasonForError(err) == metav1.StatusReasonServerTimeout
} }
// IsInternalError determines if err is an error which indicates an internal server error. // IsInternalError determines if err is an error which indicates an internal server error.
// It supports wrapped errors.
func IsInternalError(err error) bool { func IsInternalError(err error) bool {
return ReasonForError(err) == metav1.StatusReasonInternalError return ReasonForError(err) == metav1.StatusReasonInternalError
} }
// IsTooManyRequests determines if err is an error which indicates that there are too many requests // IsTooManyRequests determines if err is an error which indicates that there are too many requests
// that the server cannot handle. // that the server cannot handle.
// It supports wrapped errors.
func IsTooManyRequests(err error) bool { func IsTooManyRequests(err error) bool {
if ReasonForError(err) == metav1.StatusReasonTooManyRequests { if ReasonForError(err) == metav1.StatusReasonTooManyRequests {
return true return true
} }
switch t := err.(type) { if status := APIStatus(nil); errors.As(err, &status) {
case APIStatus: return status.Status().Code == http.StatusTooManyRequests
return t.Status().Code == http.StatusTooManyRequests
} }
return false return false
} }
// IsRequestEntityTooLargeError determines if err is an error which indicates // IsRequestEntityTooLargeError determines if err is an error which indicates
// the request entity is too large. // the request entity is too large.
// It supports wrapped errors.
func IsRequestEntityTooLargeError(err error) bool { func IsRequestEntityTooLargeError(err error) bool {
if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge { if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge {
return true return true
} }
switch t := err.(type) { if status := APIStatus(nil); errors.As(err, &status) {
case APIStatus: return status.Status().Code == http.StatusRequestEntityTooLarge
return t.Status().Code == http.StatusRequestEntityTooLarge
} }
return false return false
} }
// IsUnexpectedServerError returns true if the server response was not in the expected API format, // IsUnexpectedServerError returns true if the server response was not in the expected API format,
// and may be the result of another HTTP actor. // and may be the result of another HTTP actor.
// It supports wrapped errors.
func IsUnexpectedServerError(err error) bool { func IsUnexpectedServerError(err error) bool {
switch t := err.(type) { if status := APIStatus(nil); errors.As(err, &status) && status.Status().Details != nil {
case APIStatus: for _, cause := range status.Status().Details.Causes {
if d := t.Status().Details; d != nil { if cause.Type == metav1.CauseTypeUnexpectedServerResponse {
for _, cause := range d.Causes { return true
if cause.Type == metav1.CauseTypeUnexpectedServerResponse {
return true
}
} }
} }
} }
@ -611,38 +626,37 @@ func IsUnexpectedServerError(err error) bool {
} }
// IsUnexpectedObjectError determines if err is due to an unexpected object from the master. // IsUnexpectedObjectError determines if err is due to an unexpected object from the master.
// It supports wrapped errors.
func IsUnexpectedObjectError(err error) bool { func IsUnexpectedObjectError(err error) bool {
_, ok := err.(*UnexpectedObjectError) uoe := &UnexpectedObjectError{}
return err != nil && ok return err != nil && errors.As(err, &uoe)
} }
// SuggestsClientDelay returns true if this error suggests a client delay as well as the // SuggestsClientDelay returns true if this error suggests a client delay as well as the
// suggested seconds to wait, or false if the error does not imply a wait. It does not // suggested seconds to wait, or false if the error does not imply a wait. It does not
// address whether the error *should* be retried, since some errors (like a 3xx) may // address whether the error *should* be retried, since some errors (like a 3xx) may
// request delay without retry. // request delay without retry.
// It supports wrapped errors.
func SuggestsClientDelay(err error) (int, bool) { func SuggestsClientDelay(err error) (int, bool) {
switch t := err.(type) { if t := APIStatus(nil); errors.As(err, &t) && t.Status().Details != nil {
case APIStatus: switch t.Status().Reason {
if t.Status().Details != nil { // this StatusReason explicitly requests the caller to delay the action
switch t.Status().Reason { case metav1.StatusReasonServerTimeout:
// this StatusReason explicitly requests the caller to delay the action return int(t.Status().Details.RetryAfterSeconds), true
case metav1.StatusReasonServerTimeout: }
return int(t.Status().Details.RetryAfterSeconds), true // If the client requests that we retry after a certain number of seconds
} if t.Status().Details.RetryAfterSeconds > 0 {
// If the client requests that we retry after a certain number of seconds return int(t.Status().Details.RetryAfterSeconds), true
if t.Status().Details.RetryAfterSeconds > 0 {
return int(t.Status().Details.RetryAfterSeconds), true
}
} }
} }
return 0, false return 0, false
} }
// ReasonForError returns the HTTP status for a particular error. // ReasonForError returns the HTTP status for a particular error.
// It supports wrapped errors.
func ReasonForError(err error) metav1.StatusReason { func ReasonForError(err error) metav1.StatusReason {
switch t := err.(type) { if status := APIStatus(nil); errors.As(err, &status) {
case APIStatus: return status.Status().Reason
return t.Status().Reason
} }
return metav1.StatusReasonUnknown return metav1.StatusReasonUnknown
} }

View File

@ -19,6 +19,7 @@ package errors
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"testing" "testing"
@ -220,3 +221,260 @@ func TestFromObject(t *testing.T) {
} }
} }
} }
func TestReasonForErrorSupportsWrappedErrors(t *testing.T) {
testCases := []struct {
name string
err error
expectedReason metav1.StatusReason
}{
{
name: "Direct match",
err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonUnauthorized}},
expectedReason: metav1.StatusReasonUnauthorized,
},
{
name: "No match",
err: errors.New("some other error"),
expectedReason: metav1.StatusReasonUnknown,
},
{
name: "Nested match",
err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonAlreadyExists}})),
expectedReason: metav1.StatusReasonAlreadyExists,
},
{
name: "Nested, no match",
err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))),
expectedReason: metav1.StatusReasonUnknown,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if result := ReasonForError(tc.err); result != tc.expectedReason {
t.Errorf("expected reason: %q, but got known reason: %q", tc.expectedReason, result)
}
})
}
}
func TestIsTooManyRequestsSupportsWrappedErrors(t *testing.T) {
testCases := []struct {
name string
err error
expectMatch bool
}{
{
name: "Direct match via status reason",
err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonTooManyRequests}},
expectMatch: true,
},
{
name: "Direct match via status code",
err: &StatusError{ErrStatus: metav1.Status{Code: http.StatusTooManyRequests}},
expectMatch: true,
},
{
name: "No match",
err: &StatusError{},
expectMatch: false,
},
{
name: "Nested match via status reason",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonTooManyRequests}}),
expectMatch: true,
},
{
name: "Nested match via status code",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusTooManyRequests}}),
expectMatch: true,
},
{
name: "Nested,no match",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}),
expectMatch: false,
},
}
for _, tc := range testCases {
if result := IsTooManyRequests(tc.err); result != tc.expectMatch {
t.Errorf("Expect match %t, got match %t", tc.expectMatch, result)
}
}
}
func TestIsRequestEntityTooLargeErrorSupportsWrappedErrors(t *testing.T) {
testCases := []struct {
name string
err error
expectMatch bool
}{
{
name: "Direct match via status reason",
err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonRequestEntityTooLarge}},
expectMatch: true,
},
{
name: "Direct match via status code",
err: &StatusError{ErrStatus: metav1.Status{Code: http.StatusRequestEntityTooLarge}},
expectMatch: true,
},
{
name: "No match",
err: &StatusError{},
expectMatch: false,
},
{
name: "Nested match via status reason",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonRequestEntityTooLarge}}),
expectMatch: true,
},
{
name: "Nested match via status code",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusRequestEntityTooLarge}}),
expectMatch: true,
},
{
name: "Nested,no match",
err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}),
expectMatch: false,
},
}
for _, tc := range testCases {
if result := IsRequestEntityTooLargeError(tc.err); result != tc.expectMatch {
t.Errorf("Expect match %t, got match %t", tc.expectMatch, result)
}
}
}
func TestIsUnexpectedServerError(t *testing.T) {
unexpectedServerErr := func() error {
return &StatusError{
ErrStatus: metav1.Status{
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{{Type: metav1.CauseTypeUnexpectedServerResponse}},
},
},
}
}
testCases := []struct {
name string
err error
expectMatch bool
}{
{
name: "Direct match",
err: unexpectedServerErr(),
expectMatch: true,
},
{
name: "No match",
err: errors.New("some other error"),
expectMatch: false,
},
{
name: "Nested match",
err: fmt.Errorf("wrapping: %w", unexpectedServerErr()),
expectMatch: true,
},
{
name: "Nested, no match",
err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))),
expectMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if result := IsUnexpectedServerError(tc.err); result != tc.expectMatch {
t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result)
}
})
}
}
func TestIsUnexpectedObjectError(t *testing.T) {
unexpectedObjectErr := func() error {
return &UnexpectedObjectError{}
}
testCases := []struct {
name string
err error
expectMatch bool
}{
{
name: "Direct match",
err: unexpectedObjectErr(),
expectMatch: true,
},
{
name: "No match",
err: errors.New("some other error"),
expectMatch: false,
},
{
name: "Nested match",
err: fmt.Errorf("wrapping: %w", unexpectedObjectErr()),
expectMatch: true,
},
{
name: "Nested, no match",
err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))),
expectMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if result := IsUnexpectedObjectError(tc.err); result != tc.expectMatch {
t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result)
}
})
}
}
func TestSuggestsClientDelaySupportsWrapping(t *testing.T) {
suggestsClientDelayErr := func() error {
return &StatusError{
ErrStatus: metav1.Status{
Reason: metav1.StatusReasonServerTimeout,
Details: &metav1.StatusDetails{},
},
}
}
testCases := []struct {
name string
err error
expectMatch bool
}{
{
name: "Direct match",
err: suggestsClientDelayErr(),
expectMatch: true,
},
{
name: "No match",
err: errors.New("some other error"),
expectMatch: false,
},
{
name: "Nested match",
err: fmt.Errorf("wrapping: %w", suggestsClientDelayErr()),
expectMatch: true,
},
{
name: "Nested, no match",
err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))),
expectMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if _, result := SuggestsClientDelay(tc.err); result != tc.expectMatch {
t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result)
}
})
}
}