Skip to content

Commit

Permalink
feat(error): handle wrapped errors in IsError() (#375)
Browse files Browse the repository at this point in the history
`hcloud.IsError()` currently does a type assertion to get our error type. This breaks if the error was previously wrapped, causing it to always return "false". This happens often if the error is passed through multiple functions before error handling is being done.

Example of code that is currently broken:

```go
// Error returned by hcloud-go
err := hcloud.Error{ Code: hcloud.ErrorCodeUnauthorized }

// Error gets wrapped at some point in the call-chain
err = fmt.Errorf("failed to foobar: %w", err)

// Now the error is not properly identified
hcloud.IsError(err, hcloud.ErrorCodeUnauthorized) // false
```

---

Backport 83df108 from #374.

Co-authored-by: Julian Tölle <julian.toelle@hetzner-cloud.de>
  • Loading branch information
github-actions[bot] and apricote authored Jan 15, 2024
1 parent 991ffd8 commit 64902c5
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 1 deletion.
4 changes: 3 additions & 1 deletion hcloud/error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hcloud

import (
"errors"
"fmt"
"net"
)
Expand Down Expand Up @@ -120,7 +121,8 @@ type ErrorDetailsInvalidInputField struct {

// IsError returns whether err is an API error with the given error code.
func IsError(err error, code ErrorCode) bool {
apiErr, ok := err.(Error)
var apiErr Error
ok := errors.As(err, &apiErr)
return ok && apiErr.Code == code
}

Expand Down
101 changes: 101 additions & 0 deletions hcloud/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package hcloud

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestError_Error(t *testing.T) {
type fields struct {
Code ErrorCode
Message string
Details interface{}
response *Response
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "simple error",
fields: fields{
Code: ErrorCodeUnauthorized,
Message: "unable to authenticate",
},
want: "unable to authenticate (unauthorized)",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := Error{
Code: tt.fields.Code,
Message: tt.fields.Message,
Details: tt.fields.Details,
response: tt.fields.response,
}
assert.Equalf(t, tt.want, e.Error(), "Error()")
})
}
}

func TestIsError(t *testing.T) {
type args struct {
err error
code ErrorCode
}
tests := []struct {
name string
args args
want bool
}{
{
name: "hcloud error with code",
args: args{
err: Error{Code: ErrorCodeUnauthorized},
code: ErrorCodeUnauthorized,
},
want: true,
},
{
name: "hcloud error with different code",
args: args{
err: Error{Code: ErrorCodeConflict},
code: ErrorCodeUnauthorized,
},
want: false,
},
{
name: "wrapped hcloud error with code",
args: args{
err: fmt.Errorf("wrapped: %w", Error{Code: ErrorCodeUnauthorized}),
code: ErrorCodeUnauthorized,
},
want: true,
},
{
name: "wrapped hcloud error with different code",
args: args{
err: fmt.Errorf("wrapped: %w", Error{Code: ErrorCodeConflict}),
code: ErrorCodeUnauthorized,
},
want: false,
},
{
name: "non-hcloud error",
args: args{
err: fmt.Errorf("something went wrong"),
code: ErrorCodeUnauthorized,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, IsError(tt.args.err, tt.args.code), "IsError(%v, %v)", tt.args.err, tt.args.code)
})
}
}

0 comments on commit 64902c5

Please sign in to comment.