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