Skip to content

Better error handling for API errors#596

Merged
jacobbednarz merged 8 commits into0.14from
better-error-handling-and-use
Mar 2, 2021
Merged

Better error handling for API errors#596
jacobbednarz merged 8 commits into0.14from
better-error-handling-and-use

Conversation

@jacobbednarz
Copy link
Copy Markdown
Contributor

@jacobbednarz jacobbednarz commented Feb 24, 2021

This introduces a change to how we raise and interact with exceptions
raised by the API responses.

For quite some time, interacting with the API response errors has been
difficult due to them spitting out parsed JSON (as a string). This means
if you need to check the type of exception in order to recover or retry,
you need to use an unreliable strings.Contains check. This also
extends to internally when we want to show the user an error, we had a
errorFromResponse function which would return the HTTP status code and
whatever was in the errors array.

To improve the situation I'm proposing a revamp of our error handling by
exposing various methods on APIRequestError struct as helpers to get
the information clients may need to better handle errors. Here are some
examples:

Return the whole error message

fmt.Println(err.Error())

// => "HTTP status 400: Authentication error (10000)"

// with multiple errors returned

fmt.Println(err.Error())

// => "HTTP status 303: test (10000), test1 (10001)"

Return only the HTTP status

fmt.Println(err.(*cloudflare.APIRequestError).HTTPStatusCode())

// => 400

Check if the response was rate limited

fmt.Println(err.(*cloudflare.APIRequestError).ClientRateLimited())

// => false

Fetch just the internal error codes

e := APIRequestError{
  StatusCode: 123,
  Errors: []ResponseInfo{
    {
      Message: "test",
      Code:    10000,
    },
    {
      Message: "test1",
      Code:    10001,
    },
  },
}

fmt.Printf("%#v", e.InternalErrorCodes())

// => []int{10000, 10001}

// or

fmt.Println(err.(*cloudflare.APIRequestError).InternalErrorCodes())

// => []int{10000, 10001}

In order to achieve this, we'll need to update almost every method that
makes an API call to reflect the following (example from
createAccessRule in firewall.go):

@@ -218,7 +218,7 @@ func (api *API) createAccessRule(prefix string, accessRule AccessRule) (*AccessR
        uri := prefix + "/firewall/access_rules/rules"
        res, err := api.makeRequest("POST", uri, accessRule)
        if err != nil {
-               return nil, errors.Wrap(err, errMakeRequestError)
+               return nil, err
        }

        response := &AccessRuleResponse{}

Personally, I think this is a better situation for the maintainers and
consumers of this library as we'll be better equipped to handle
differing error scenarios. The Terraform provider will also benefit from
these changes for things like retries.

Recent issues related to this (more upstream in Terraform):

I haven't gone ahead and fixed any tests yet but if we agree this is
a good move, I'll take the usual steps with CI.

This introduces a change to how we raise and interact with exceptions
raised by the API responses.

For quite some time, interacting with the API response errors has been
difficult due to them spitting out parsed JSON (as a string). This means
if you need to check the type of exception in order to recover or retry,
you need to use an unreliable `strings.Contains` check. This also
extends to internally when we want to show the user an error, we had a
`errorFromResponse` function which would return the HTTP status code and
whatever was in the `errors` array.

To improve the situation I'm proposing a revamp of our error handling by
exposing various methods on `APIRequestError` struct as helpers to get
the information clients may need to better handle errors. Here are some
examples:

### Return the whole error message

```go
fmt.Println(err.Error())

// => "HTTP status 400: Authentication error (10000)"
```

### Return only the HTTP status

```go
fmt.Println(err.(*cloudflare.APIRequestError).HTTPStatusCode())

// => 400
```

### Check if the response was rate limited

```go
fmt.Println(err.(*cloudflare.APIRequestError).HTTPStatusCode())

// => false
```

In order to achieve this, we'll need to update almost every method that
makes an API call to reflect the following (example from
`createAccessRule` in firewall.go):

```diff
@@ -218,7 +218,7 @@ func (api *API) createAccessRule(prefix string, accessRule AccessRule) (*AccessR
        uri := prefix + "/firewall/access_rules/rules"
        res, err := api.makeRequest("POST", uri, accessRule)
        if err != nil {
-               return nil, errors.Wrap(err, errMakeRequestError)
+               return nil, err.(*APIRequestError)
        }

        response := &AccessRuleResponse{}
```

Personally, I think this is a better situation for the maintainers and
consumers of this library as we'll be better equipped to handle
differing error scenarios. The Terraform provider will also benefit from
these changes for things like retries.
@jacobbednarz
Copy link
Copy Markdown
Contributor Author

@jsha @firefart would you folks mind chucking your 2 cents in here as well whether this jives for your uses?

@jacobbednarz jacobbednarz changed the base branch from master to 0.14 February 24, 2021 01:40
@jacobbednarz jacobbednarz added this to the v0.14 milestone Feb 24, 2021
Comment thread cloudflare.go Outdated
Updates the functions to assume > 1 error code may be returned in a single API response
Copy link
Copy Markdown

@patryk patryk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the idea of concatenating multiple messages while stringifying.

@jacobbednarz
Copy link
Copy Markdown
Contributor Author

Awesome, let me add some test coverage and update the methods themselves and we can get this shipped.

Copy link
Copy Markdown

@firefart firefart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick implementation @jacobbednarz !
Left you some comments on the changes

Comment thread errors.go Outdated
Comment thread cloudflare.go
@jacobbednarz
Copy link
Copy Markdown
Contributor Author

@firefart for your specific use case raised in #574, I envision your code looking something like this:

resp, err := c.client.CreateAccountAccessRule(accountID, cloudflare.AccessRule{
  Mode:  "block",
  Notes: comment,
  Configuration: cloudflare.AccessRuleConfiguration{
    Target: target,
    Value:  ip,
  },
})

// BYO contains function 
if err != nil {
  if contains(err.(*APIRequestError).InternalErrorCodes(), 10009) {
    // fine to ignore
  } else {
    // better look into the error
  }
}

// ...or just access the index position directly
if err != nil {
  if err.(*APIRequestError).InternalErrorCodes()[0] == 10009 {
    // fine to ignore
  } else {
    // better look into the error
  }
}

@jacobbednarz jacobbednarz merged commit c237d53 into 0.14 Mar 2, 2021
@jacobbednarz jacobbednarz deleted the better-error-handling-and-use branch March 2, 2021 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants