Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a method for ferrying API errors back to the caller #265

Merged
merged 1 commit into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,46 @@ If you need to use your own HTTP client, for doing things like defining your own
transport settings, you can replace the default HTTP client with your own by
simply by setting a new value in the `HTTPClient` field.

#### API Error Responses

For cases where your request results in an error from the API, you can use the
`errors.As()` function from the standard library to extract the
`pagerduty.APIError` error value and inspect more details about the error,
including the HTTP response code and PagerDuty API Error Code.

```go
package main

import (
"fmt"
"github.com/PagerDuty/go-pagerduty"
)

var authtoken = "" // Set your auth token here

func main() {
client := pagerduty.NewClient(authtoken)
user, err := client.GetUser("NOTREAL", pagerduty.GetUserOptions{})
if err != nil {
var aerr pagerduty.APIError

if errors.As(err, &aerr) {
if aerr.RateLimited() {
fmt.Println("rate limited")
return
}

fmt.Println("unknown status code:", aerr.StatusCode)

return
}

panic(err)
}
fmt.Println(user)
}
```

## Contributing

1. Fork it ( https://github.com/PagerDuty/go-pagerduty/fork )
Expand Down
116 changes: 96 additions & 20 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
"runtime"
"strings"
"time"
)

Expand Down Expand Up @@ -56,10 +57,73 @@ type APIDetails struct {
Details string `json:"details,omitempty"`
}

type errorObject struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Errors interface{} `json:"errors,omitempty"`
stmcallister marked this conversation as resolved.
Show resolved Hide resolved
// APIErrorObject represents the object returned by the API when an error
// occurs. This includes messages that should hopefully provide useful context
// to the end user.
type APIErrorObject struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Errors []string `json:"errors,omitempty"`
}

// APIError represents the error response received when an API call fails. The
// HTTP response code is set inside of the StatusCode field, with the APIError
// field being the structured JSON error object returned from the API.
//
// This type also provides some helper methods like .RateLimited(), .NotFound(),
// and .Temporary() to help callers reason about how to handle the error.
//
// You can read more about the HTTP status codes and API error codes returned
// from the API here: https://developer.pagerduty.com/docs/rest-api-v2/errors/
type APIError struct {
// StatusCode is the HTTP response status code
StatusCode int `json:"-"`

// APIError represents the object returned by the API when an error occurs.
// If the response has no error object present, this will be nil.
//
// This includes messages that should hopefully provide useful context to
// the end user.
APIError *APIErrorObject `json:"error"`

message string
}

// Error satisfies the error interface, and should contain the StatusCode,
// APIErrorObject.Message, and APIErrorObject.Code.
func (a APIError) Error() string {
if len(a.message) > 0 {
return a.message
}

if a.APIError == nil {
return fmt.Sprintf("HTTP response failed with status code %d and no JSON error object was present", a.StatusCode)
}

return fmt.Sprintf(
"HTTP response failed with status code %d, message: %s (code: %d)",
a.StatusCode, a.APIError.Message, a.APIError.Code,
)
}

// RateLimited returns whether the response had a status of 429, and as such the
// client is rate limited. The PagerDuty rate limits should reset once per
// minute, and for the REST API they are an account-wide rate limit (not per
// API key or IP).
func (a APIError) RateLimited() bool {
return a.StatusCode == http.StatusTooManyRequests
}

// Temporary returns whether it was a temporary error, one of which is a
// RateLimited error.
func (a APIError) Temporary() bool {
return a.RateLimited() || (a.StatusCode >= 500 && a.StatusCode < 600)
}

// NotFound returns whether this was an error where it seems like the resource
// was not found.
func (a APIError) NotFound() bool {
return a.StatusCode == http.StatusNotFound || a.APIError.Code == 2100
}

func newDefaultHTTPClient() *http.Client {
Expand Down Expand Up @@ -161,7 +225,6 @@ func (c *Client) delete(path string) (*http.Response, error) {
}

func (c *Client) put(path string, payload interface{}, headers *map[string]string) (*http.Response, error) {

if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
Expand Down Expand Up @@ -224,27 +287,40 @@ func (c *Client) checkResponse(resp *http.Response, err error) (*http.Response,
if err != nil {
return resp, fmt.Errorf("Error calling the API endpoint: %v", err)
}
if 199 >= resp.StatusCode || 300 <= resp.StatusCode {
var eo *errorObject
var getErr error
if eo, getErr = c.getErrorFromResponse(resp); getErr != nil {
return resp, fmt.Errorf("Response did not contain formatted error: %s. HTTP response code: %v. Raw response: %+v", getErr, resp.StatusCode, resp)
}
return resp, fmt.Errorf("Failed call API endpoint. HTTP response code: %v. Error: %v", resp.StatusCode, eo)

if resp.StatusCode < 200 || resp.StatusCode > 299 {
return resp, c.getErrorFromResponse(resp)
}

return resp, nil
}

func (c *Client) getErrorFromResponse(resp *http.Response) (*errorObject, error) {
var result map[string]errorObject
if err := c.decodeJSON(resp, &result); err != nil {
return nil, fmt.Errorf("Could not decode JSON response: %v", err)
func (c *Client) getErrorFromResponse(resp *http.Response) APIError {
// check whether the error response is declared as JSON
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
aerr := APIError{
StatusCode: resp.StatusCode,
message: fmt.Sprintf("HTTP response with status code %d does not contain Content-Type: application/json", resp.StatusCode),
}

return aerr
}
s, ok := result["error"]
if !ok {
return nil, fmt.Errorf("JSON response does not have error field")

var document APIError

// because of above check this probably won't fail, but it's possible...
if err := c.decodeJSON(resp, &document); err != nil {
aerr := APIError{
StatusCode: resp.StatusCode,
message: fmt.Sprintf("HTTP response with status code %d, JSON error object decode failed: %s", resp.StatusCode, err),
}

return aerr
}
return &s, nil

document.StatusCode = resp.StatusCode

return document
}

// responseHandler is capable of parsing a response. At a minimum it must
Expand Down
Loading