errors.go raw
1 package scw
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/scaleway/scaleway-sdk-go/errors"
13 "github.com/scaleway/scaleway-sdk-go/validation"
14 )
15
16 // SdkError is a base interface for all Scaleway SDK errors.
17 type SdkError interface {
18 Error() string
19 IsScwSdkError()
20 }
21
22 // ResponseError is an error type for the Scaleway API
23 type ResponseError struct {
24 // Message is a human-friendly error message
25 Message string `json:"message"`
26
27 // Type is a string code that defines the kind of error. This field is only used by instance API
28 Type string `json:"type,omitempty"`
29
30 // Resource is a string code that defines the resource concerned by the error. This field is only used by instance API
31 Resource string `json:"resource,omitempty"`
32
33 // Fields contains detail about validation error. This field is only used by instance API
34 Fields map[string][]string `json:"fields,omitempty"`
35
36 // StatusCode is the HTTP status code received
37 StatusCode int `json:"-"`
38
39 // Status is the HTTP status received
40 Status string `json:"-"`
41
42 RawBody json.RawMessage `json:"-"`
43 }
44
45 func (e *ResponseError) UnmarshalJSON(b []byte) error {
46 type tmpResponseError ResponseError
47 tmp := tmpResponseError(*e)
48
49 err := json.Unmarshal(b, &tmp)
50 if err != nil {
51 return err
52 }
53 *e = ResponseError(tmp)
54 return nil
55 }
56
57 // IsScwSdkError implement SdkError interface
58 func (e *ResponseError) IsScwSdkError() {}
59
60 func (e *ResponseError) Error() string {
61 s := "scaleway-sdk-go: http error " + e.Status
62
63 if e.Resource != "" {
64 s = fmt.Sprintf("%s: resource %s", s, e.Resource)
65 }
66
67 if e.Message != "" {
68 s = fmt.Sprintf("%s: %s", s, e.Message)
69 }
70
71 if len(e.Fields) > 0 {
72 s = fmt.Sprintf("%s: %v", s, e.Fields)
73 }
74
75 return s
76 }
77
78 func (e *ResponseError) GetRawBody() json.RawMessage {
79 return e.RawBody
80 }
81
82 // hasResponseError returns an SdkError when the HTTP status is not OK.
83 func hasResponseError(res *http.Response) error {
84 if res.StatusCode >= 200 && res.StatusCode <= 299 {
85 return nil
86 }
87
88 newErr := &ResponseError{
89 StatusCode: res.StatusCode,
90 Status: res.Status,
91 }
92
93 if res.Body == nil {
94 return newErr
95 }
96
97 body, err := io.ReadAll(res.Body)
98 if err != nil {
99 return errors.Wrap(err, "cannot read error response body")
100 }
101 newErr.RawBody = body
102
103 // The error content is not encoded in JSON, only returns HTTP data.
104 contentType := res.Header.Get("Content-Type")
105 if !strings.HasPrefix(contentType, "application/json") {
106 newErr.Message = res.Status
107 return newErr
108 }
109
110 err = json.Unmarshal(body, newErr)
111 if err != nil {
112 return errors.Wrap(err, "could not parse error response body")
113 }
114
115 err = unmarshalStandardError(newErr.Type, body)
116 if err != nil {
117 return err
118 }
119
120 err = unmarshalNonStandardError(newErr.Type, body)
121 if err != nil {
122 return err
123 }
124
125 return newErr
126 }
127
128 func unmarshalStandardError(errorType string, body []byte) error {
129 var stdErr SdkError
130
131 switch errorType {
132 case "invalid_arguments":
133 stdErr = &InvalidArgumentsError{RawBody: body}
134 case "quotas_exceeded":
135 stdErr = &QuotasExceededError{RawBody: body}
136 case "transient_state":
137 stdErr = &TransientStateError{RawBody: body}
138 case "not_found":
139 stdErr = &ResourceNotFoundError{RawBody: body}
140 case "locked":
141 stdErr = &ResourceLockedError{RawBody: body}
142 case "permissions_denied":
143 stdErr = &PermissionsDeniedError{RawBody: body}
144 case "out_of_stock":
145 stdErr = &OutOfStockError{RawBody: body}
146 case "resource_expired":
147 stdErr = &ResourceExpiredError{RawBody: body}
148 case "denied_authentication":
149 stdErr = &DeniedAuthenticationError{RawBody: body}
150 case "precondition_failed":
151 stdErr = &PreconditionFailedError{RawBody: body}
152 default:
153 return nil
154 }
155
156 err := json.Unmarshal(body, stdErr)
157 if err != nil {
158 return errors.Wrap(err, "could not parse error %s response body", errorType)
159 }
160
161 return stdErr
162 }
163
164 func unmarshalNonStandardError(errorType string, body []byte) error {
165 switch errorType {
166 // Only in instance API.
167
168 case "unknown_resource":
169 unknownResourceError := &UnknownResource{RawBody: body}
170 err := json.Unmarshal(body, unknownResourceError)
171 if err != nil {
172 return errors.Wrap(err, "could not parse error %s response body", errorType)
173 }
174 return unknownResourceError.ToResourceNotFoundError()
175
176 case "invalid_request_error":
177 invalidRequestError := &InvalidRequestError{RawBody: body}
178 err := json.Unmarshal(body, invalidRequestError)
179 if err != nil {
180 return errors.Wrap(err, "could not parse error %s response body", errorType)
181 }
182
183 invalidArgumentsError := invalidRequestError.ToInvalidArgumentsError()
184 if invalidArgumentsError != nil {
185 return invalidArgumentsError
186 }
187
188 quotasExceededError := invalidRequestError.ToQuotasExceededError()
189 if quotasExceededError != nil {
190 return quotasExceededError
191 }
192
193 // At this point, the invalid_request_error is not an InvalidArgumentsError and
194 // the default marshalling will be used.
195 return nil
196
197 default:
198 return nil
199 }
200 }
201
202 type InvalidArgumentsErrorDetail struct {
203 ArgumentName string `json:"argument_name"`
204 Reason string `json:"reason"`
205 HelpMessage string `json:"help_message"`
206 }
207
208 type InvalidArgumentsError struct {
209 Details []InvalidArgumentsErrorDetail `json:"details"`
210
211 RawBody json.RawMessage `json:"-"`
212 }
213
214 // IsScwSdkError implements the SdkError interface
215 func (e *InvalidArgumentsError) IsScwSdkError() {}
216
217 func (e *InvalidArgumentsError) Error() string {
218 invalidArgs := make([]string, len(e.Details))
219 for i, d := range e.Details {
220 invalidArgs[i] = d.ArgumentName
221 switch d.Reason {
222 case "unknown":
223 invalidArgs[i] += " is invalid for unexpected reason"
224 case "required":
225 invalidArgs[i] += " is required"
226 case "format":
227 invalidArgs[i] += " is wrongly formatted"
228 case "constraint":
229 invalidArgs[i] += " does not respect constraint"
230 }
231 if d.HelpMessage != "" {
232 invalidArgs[i] += ", " + d.HelpMessage
233 }
234 }
235
236 return "scaleway-sdk-go: invalid argument(s): " + strings.Join(invalidArgs, "; ")
237 }
238
239 func (e *InvalidArgumentsError) GetRawBody() json.RawMessage {
240 return e.RawBody
241 }
242
243 // UnknownResource is only returned by the instance API.
244 // Warning: this is not a standard error.
245 type UnknownResource struct {
246 Message string `json:"message"`
247 RawBody json.RawMessage `json:"-"`
248 }
249
250 // ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
251 func (e *UnknownResource) ToResourceNotFoundError() *ResourceNotFoundError {
252 resourceNotFound := &ResourceNotFoundError{
253 RawBody: e.RawBody,
254 }
255
256 messageParts := strings.Split(e.Message, `"`)
257
258 // Some errors uses ' and not "
259 if len(messageParts) == 1 {
260 messageParts = strings.Split(e.Message, "'")
261 }
262
263 switch len(messageParts) {
264 case 2: // message like: `"111..." not found`
265 resourceNotFound.ResourceID = messageParts[0]
266 case 3: // message like: `Security Group "111..." not found`
267 resourceNotFound.ResourceID = messageParts[1]
268 // transform `Security group ` to `security_group`
269 resourceNotFound.Resource = strings.ReplaceAll(strings.ToLower(strings.TrimSpace(messageParts[0])), " ", "_")
270 default:
271 return nil
272 }
273 if !validation.IsUUID(resourceNotFound.ResourceID) {
274 return nil
275 }
276 return resourceNotFound
277 }
278
279 // InvalidRequestError is only returned by the instance API.
280 // Warning: this is not a standard error.
281 type InvalidRequestError struct {
282 Message string `json:"message"`
283
284 Fields map[string][]string `json:"fields"`
285
286 Resource string `json:"resource"`
287
288 RawBody json.RawMessage `json:"-"`
289 }
290
291 // ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
292 func (e *InvalidRequestError) ToInvalidArgumentsError() *InvalidArgumentsError {
293 // If error has no fields, it is not an InvalidArgumentsError.
294 if len(e.Fields) == 0 {
295 return nil
296 }
297
298 invalidArguments := &InvalidArgumentsError{
299 RawBody: e.RawBody,
300 }
301 fieldNames := []string(nil)
302 for fieldName := range e.Fields {
303 fieldNames = append(fieldNames, fieldName)
304 }
305 sort.Strings(fieldNames)
306 for _, fieldName := range fieldNames {
307 for _, message := range e.Fields[fieldName] {
308 invalidArguments.Details = append(invalidArguments.Details, InvalidArgumentsErrorDetail{
309 ArgumentName: fieldName,
310 Reason: "constraint",
311 HelpMessage: message,
312 })
313 }
314 }
315 return invalidArguments
316 }
317
318 func (e *InvalidRequestError) ToQuotasExceededError() *QuotasExceededError {
319 if !strings.Contains(strings.ToLower(e.Message), "quota exceeded for this resource") {
320 return nil
321 }
322
323 return &QuotasExceededError{
324 Details: []QuotasExceededErrorDetail{
325 {
326 Resource: e.Resource,
327 Quota: 0,
328 Current: 0,
329 },
330 },
331 RawBody: e.RawBody,
332 }
333 }
334
335 type QuotasExceededErrorDetail struct {
336 Resource string `json:"resource"`
337 Quota uint32 `json:"quota"`
338 Current uint32 `json:"current"`
339 }
340
341 type QuotasExceededError struct {
342 Details []QuotasExceededErrorDetail `json:"details"`
343 RawBody json.RawMessage `json:"-"`
344 }
345
346 // IsScwSdkError implements the SdkError interface
347 func (e *QuotasExceededError) IsScwSdkError() {}
348
349 func (e *QuotasExceededError) Error() string {
350 invalidArgs := make([]string, len(e.Details))
351 for i, d := range e.Details {
352 invalidArgs[i] = fmt.Sprintf("%s has reached its quota (%d/%d)", d.Resource, d.Current, d.Quota)
353 }
354
355 return "scaleway-sdk-go: quota exceeded(s): " + strings.Join(invalidArgs, "; ")
356 }
357
358 func (e *QuotasExceededError) GetRawBody() json.RawMessage {
359 return e.RawBody
360 }
361
362 type PermissionsDeniedError struct {
363 Details []struct {
364 Resource string `json:"resource"`
365 Action string `json:"action"`
366 } `json:"details"`
367
368 RawBody json.RawMessage `json:"-"`
369 }
370
371 // IsScwSdkError implements the SdkError interface
372 func (e *PermissionsDeniedError) IsScwSdkError() {}
373
374 func (e *PermissionsDeniedError) Error() string {
375 invalidArgs := make([]string, len(e.Details))
376 for i, d := range e.Details {
377 invalidArgs[i] = fmt.Sprintf("%s %s", d.Action, d.Resource)
378 }
379
380 return "scaleway-sdk-go: insufficient permissions: " + strings.Join(invalidArgs, "; ")
381 }
382
383 func (e *PermissionsDeniedError) GetRawBody() json.RawMessage {
384 return e.RawBody
385 }
386
387 type TransientStateError struct {
388 Resource string `json:"resource"`
389 ResourceID string `json:"resource_id"`
390 CurrentState string `json:"current_state"`
391
392 RawBody json.RawMessage `json:"-"`
393 }
394
395 // IsScwSdkError implements the SdkError interface
396 func (e *TransientStateError) IsScwSdkError() {}
397
398 func (e *TransientStateError) Error() string {
399 return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is in a transient state: %s", e.Resource, e.ResourceID, e.CurrentState)
400 }
401
402 func (e *TransientStateError) GetRawBody() json.RawMessage {
403 return e.RawBody
404 }
405
406 type ResourceNotFoundError struct {
407 Resource string `json:"resource"`
408 ResourceID string `json:"resource_id"`
409
410 RawBody json.RawMessage `json:"-"`
411 }
412
413 // IsScwSdkError implements the SdkError interface
414 func (e *ResourceNotFoundError) IsScwSdkError() {}
415
416 func (e *ResourceNotFoundError) Error() string {
417 return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is not found", e.Resource, e.ResourceID)
418 }
419
420 func (e *ResourceNotFoundError) GetRawBody() json.RawMessage {
421 return e.RawBody
422 }
423
424 type ResourceLockedError struct {
425 Resource string `json:"resource"`
426 ResourceID string `json:"resource_id"`
427
428 RawBody json.RawMessage `json:"-"`
429 }
430
431 // IsScwSdkError implements the SdkError interface
432 func (e *ResourceLockedError) IsScwSdkError() {}
433
434 func (e *ResourceLockedError) Error() string {
435 return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is locked", e.Resource, e.ResourceID)
436 }
437
438 func (e *ResourceLockedError) GetRawBody() json.RawMessage {
439 return e.RawBody
440 }
441
442 type OutOfStockError struct {
443 Resource string `json:"resource"`
444
445 RawBody json.RawMessage `json:"-"`
446 }
447
448 // IsScwSdkError implements the SdkError interface
449 func (e *OutOfStockError) IsScwSdkError() {}
450
451 func (e *OutOfStockError) Error() string {
452 return fmt.Sprintf("scaleway-sdk-go: resource %s is out of stock", e.Resource)
453 }
454
455 func (e *OutOfStockError) GetRawBody() json.RawMessage {
456 return e.RawBody
457 }
458
459 // InvalidClientOptionError indicates that at least one of client data has been badly provided for the client creation.
460 type InvalidClientOptionError struct {
461 errorType string
462 }
463
464 func NewInvalidClientOptionError(format string, a ...any) *InvalidClientOptionError {
465 return &InvalidClientOptionError{errorType: fmt.Sprintf(format, a...)}
466 }
467
468 // IsScwSdkError implements the SdkError interface
469 func (e InvalidClientOptionError) IsScwSdkError() {}
470
471 func (e InvalidClientOptionError) Error() string {
472 return "scaleway-sdk-go: " + e.errorType
473 }
474
475 // ConfigFileNotFound indicates that the config file could not be found
476 type ConfigFileNotFoundError struct {
477 path string
478 }
479
480 func configFileNotFound(path string) *ConfigFileNotFoundError {
481 return &ConfigFileNotFoundError{path: path}
482 }
483
484 // ConfigFileNotFoundError implements the SdkError interface
485 func (e ConfigFileNotFoundError) IsScwSdkError() {}
486
487 func (e ConfigFileNotFoundError) Error() string {
488 return fmt.Sprintf("scaleway-sdk-go: cannot read config file %s: no such file or directory", e.path)
489 }
490
491 // ResourceExpiredError implements the SdkError interface
492 type ResourceExpiredError struct {
493 Resource string `json:"resource"`
494 ResourceID string `json:"resource_id"`
495 ExpiredSince time.Time `json:"expired_since"`
496
497 RawBody json.RawMessage `json:"-"`
498 }
499
500 func (r ResourceExpiredError) Error() string {
501 return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s expired since %s", r.Resource, r.ResourceID, r.ExpiredSince.String())
502 }
503
504 func (r ResourceExpiredError) IsScwSdkError() {}
505
506 // DeniedAuthenticationError implements the SdkError interface
507 type DeniedAuthenticationError struct {
508 Method string `json:"method"`
509 Reason string `json:"reason"`
510
511 RawBody json.RawMessage `json:"-"`
512 }
513
514 func (r DeniedAuthenticationError) Error() string {
515 var reason string
516 var method string
517
518 switch r.Method {
519 case "unknown_method":
520 method = "unknown method"
521 case "jwt":
522 method = "JWT"
523 case "api_key":
524 method = "API key"
525 }
526
527 switch r.Reason {
528 case "unknown_reason":
529 reason = "unknown reason"
530 case "invalid_argument":
531 reason = "invalid " + method + " format or empty value"
532 case "not_found":
533 reason = method + " does not exist"
534 case "expired":
535 reason = method + " is expired"
536 }
537 return "scaleway-sdk-go: denied authentication: " + reason
538 }
539
540 func (r DeniedAuthenticationError) IsScwSdkError() {}
541
542 // PreconditionFailedError implements the SdkError interface
543 type PreconditionFailedError struct {
544 Precondition string `json:"precondition"`
545 HelpMessage string `json:"help_message"`
546
547 RawBody json.RawMessage `json:"-"`
548 }
549
550 func (r PreconditionFailedError) Error() string {
551 var msg string
552 switch r.Precondition {
553 case "unknown_precondition":
554 msg = "unknown precondition"
555 case "resource_still_in_use":
556 msg = "resource is still in use"
557 case "attribute_must_be_set":
558 msg = "attribute must be set"
559 }
560 if r.HelpMessage != "" {
561 msg += ", " + r.HelpMessage
562 }
563
564 return "scaleway-sdk-go: precondition failed: " + msg
565 }
566
567 func (r PreconditionFailedError) IsScwSdkError() {}
568