-
Notifications
You must be signed in to change notification settings - Fork 81
/
errors.go
235 lines (194 loc) · 6.23 KB
/
errors.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package linodego
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"github.com/go-resty/resty/v2"
)
const (
ErrorUnsupported = iota
// ErrorFromString is the Code identifying Errors created by string types
ErrorFromString
// ErrorFromError is the Code identifying Errors created by error types
ErrorFromError
// ErrorFromStringer is the Code identifying Errors created by fmt.Stringer types
ErrorFromStringer
)
// Error wraps the LinodeGo error with the relevant http.Response
type Error struct {
Response *http.Response
Code int
Message string
}
// APIErrorReason is an individual invalid request message returned by the Linode API
type APIErrorReason struct {
Reason string `json:"reason"`
Field string `json:"field"`
}
func (r APIErrorReason) Error() string {
if len(r.Field) == 0 {
return r.Reason
}
return fmt.Sprintf("[%s] %s", r.Field, r.Reason)
}
// APIError is the error-set returned by the Linode API when presented with an invalid request
type APIError struct {
Errors []APIErrorReason `json:"errors"`
}
// String returns the error reason in a formatted string
func (r APIErrorReason) String() string {
return fmt.Sprintf("[%s] %s", r.Field, r.Reason)
}
func coupleAPIErrors(r *resty.Response, err error) (*resty.Response, error) {
if err != nil {
// an error was raised in go code, no need to check the resty Response
return nil, NewError(err)
}
if r.Error() == nil {
// no error in the resty Response
return r, nil
}
// handle the resty Response errors
// Check that response is of the correct content-type before unmarshalling
expectedContentType := r.Request.Header.Get("Accept")
responseContentType := r.Header().Get("Content-Type")
// If the upstream Linode API server being fronted fails to respond to the request,
// the http server will respond with a default "Bad Gateway" page with Content-Type
// "text/html".
if r.StatusCode() == http.StatusBadGateway && responseContentType == "text/html" { //nolint:goconst
return nil, Error{Code: http.StatusBadGateway, Message: http.StatusText(http.StatusBadGateway)}
}
if responseContentType != expectedContentType {
msg := fmt.Sprintf(
"Unexpected Content-Type: Expected: %v, Received: %v\nResponse body: %s",
expectedContentType,
responseContentType,
string(r.Body()),
)
return nil, Error{Code: r.StatusCode(), Message: msg}
}
apiError, ok := r.Error().(*APIError)
if !ok || (ok && len(apiError.Errors) == 0) {
return r, nil
}
return nil, NewError(r)
}
//nolint:unused
func coupleAPIErrorsHTTP(resp *http.Response, err error) (*http.Response, error) {
if err != nil {
// an error was raised in go code, no need to check the http.Response
return nil, NewError(err)
}
if resp == nil || resp.StatusCode < 200 || resp.StatusCode >= 300 {
// Check that response is of the correct content-type before unmarshalling
expectedContentType := resp.Request.Header.Get("Accept")
responseContentType := resp.Header.Get("Content-Type")
// If the upstream server fails to respond to the request,
// the http server will respond with a default error page with Content-Type "text/html".
if resp.StatusCode == http.StatusBadGateway && responseContentType == "text/html" { //nolint:goconst
return nil, Error{Code: http.StatusBadGateway, Message: http.StatusText(http.StatusBadGateway)}
}
if responseContentType != expectedContentType {
bodyBytes, _ := io.ReadAll(resp.Body)
msg := fmt.Sprintf(
"Unexpected Content-Type: Expected: %v, Received: %v\nResponse body: %s",
expectedContentType,
responseContentType,
string(bodyBytes),
)
return nil, Error{Code: resp.StatusCode, Message: msg}
}
var apiError APIError
if err := json.NewDecoder(resp.Body).Decode(&apiError); err != nil {
return nil, NewError(fmt.Errorf("failed to decode response body: %w", err))
}
if len(apiError.Errors) == 0 {
return resp, nil
}
return nil, Error{Code: resp.StatusCode, Message: apiError.Errors[0].String()}
}
// no error in the http.Response
return resp, nil
}
func (e APIError) Error() string {
x := []string{}
for _, msg := range e.Errors {
x = append(x, msg.Error())
}
return strings.Join(x, "; ")
}
func (err Error) Error() string {
return fmt.Sprintf("[%03d] %s", err.Code, err.Message)
}
func (err Error) StatusCode() int {
return err.Code
}
func (err Error) Is(target error) bool {
if x, ok := target.(interface{ StatusCode() int }); ok || errors.As(target, &x) {
return err.StatusCode() == x.StatusCode()
}
return false
}
// NewError creates a linodego.Error with a Code identifying the source err type,
// - ErrorFromString (1) from a string
// - ErrorFromError (2) for an error
// - ErrorFromStringer (3) for a Stringer
// - HTTP Status Codes (100-600) for a resty.Response object
func NewError(err any) *Error {
if err == nil {
return nil
}
switch e := err.(type) {
case *Error:
return e
case *resty.Response:
apiError, ok := e.Error().(*APIError)
if !ok {
return &Error{Code: ErrorUnsupported, Message: "Unexpected Resty Error Response, no error"}
}
return &Error{
Code: e.RawResponse.StatusCode,
Message: apiError.Error(),
Response: e.RawResponse,
}
case error:
return &Error{Code: ErrorFromError, Message: e.Error()}
case string:
return &Error{Code: ErrorFromString, Message: e}
case fmt.Stringer:
return &Error{Code: ErrorFromStringer, Message: e.String()}
default:
return &Error{Code: ErrorUnsupported, Message: fmt.Sprintf("Unsupported type to linodego.NewError: %s", reflect.TypeOf(e))}
}
}
// IsNotFound indicates if err indicates a 404 Not Found error from the Linode API.
func IsNotFound(err error) bool {
return ErrHasStatus(err, http.StatusNotFound)
}
// ErrHasStatus checks if err is an error from the Linode API, and whether it contains the given HTTP status code.
// More than one status code may be given.
// If len(code) == 0, err is nil or is not a [Error], ErrHasStatus will return false.
func ErrHasStatus(err error, code ...int) bool {
if err == nil {
return false
}
// Short-circuit if the caller did not provide any status codes.
if len(code) == 0 {
return false
}
var e *Error
if !errors.As(err, &e) {
return false
}
ec := e.StatusCode()
for _, c := range code {
if ec == c {
return true
}
}
return false
}