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

Return custom error type for Events API (V2) calls #419

Merged
merged 2 commits into from
Feb 5, 2022
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
8 changes: 5 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const (
)

// APIObject represents generic api json response that is shared by most
// domain object (like escalation
// domain objects (like escalation)
type APIObject struct {
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Expand Down Expand Up @@ -107,6 +107,8 @@ type NullAPIErrorObject struct {
ErrorObject APIErrorObject
}

var _ json.Unmarshaler = (*NullAPIErrorObject)(nil) // assert that it satisfies the json.Unmarshaler interface.

// UnmarshalJSON satisfies encoding/json.Unmarshaler
func (n *NullAPIErrorObject) UnmarshalJSON(data []byte) error {
var aeo APIErrorObject
Expand Down Expand Up @@ -348,7 +350,7 @@ const (
DebugCaptureLastRequest DebugFlag = 1 << 0

// DebugCaptureLastResponse captures the last HTTP response from the API (if
// there was one) and makes it available via the LastAPIReponse() method.
// there was one) and makes it available via the LastAPIResponse() method.
//
// This may increase memory usage / GC, as we'll be making a copy of the
// full HTTP response body on each request and capturing it for inspection.
Expand Down Expand Up @@ -575,7 +577,7 @@ func (c *Client) decodeJSON(resp *http.Response, payload interface{}) error {

func (c *Client) checkResponse(resp *http.Response, err error) (*http.Response, error) {
if err != nil {
return resp, fmt.Errorf("Error calling the API endpoint: %v", err)
return resp, fmt.Errorf("error calling the API endpoint: %v", err)
}

if resp.StatusCode < 200 || resp.StatusCode > 299 {
Expand Down
141 changes: 136 additions & 5 deletions event_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,125 @@ func ManageEvent(e V2Event) (*V2EventResponse, error) {
return ManageEventWithContext(context.Background(), e)
}

// EventsAPIV2Error represents the error response received when an Events API V2 call fails. The
// HTTP response code is set inside of the StatusCode field, with the EventsAPIV2Error
// field being the wrapper around the JSON error object returned from the Events API V2.
//
// This type also provides some helper methods like .BadRequest(), .RateLimited(),
// and .Temporary() to help callers reason about how to handle the error.
type EventsAPIV2Error struct {
// StatusCode is the HTTP response status code.
StatusCode int `json:"-"`

// APIError represents the object returned by the API when an error occurs,
// which includes messages that should hopefully provide useful context
// to the end user.
//
// If the API response did not contain an error object, the .Valid field of
// APIError will be false. If .Valid is true, the .ErrorObject field is
// valid and should be consulted.
APIError NullEventsAPIV2ErrorObject

message string
}

var _ json.Unmarshaler = (*EventsAPIV2Error)(nil) // assert that it satisfies the json.Unmarshaler interface.

// Error satisfies the error interface, and should contain the StatusCode,
// APIError.Message, APIError.ErrorObject.Status, and APIError.Errors.
func (e EventsAPIV2Error) Error() string {
if len(e.message) > 0 {
return e.message
}

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

if len(e.APIError.ErrorObject.Errors) == 0 {
return fmt.Sprintf(
"HTTP response failed with status code %d, status: %s, message: %s",
e.StatusCode, e.APIError.ErrorObject.Status, e.APIError.ErrorObject.Message,
)
}

return fmt.Sprintf(
"HTTP response failed with status code %d, status: %s, message: %s: %s",
e.StatusCode,
e.APIError.ErrorObject.Status,
e.APIError.ErrorObject.Message,
apiErrorsDetailString(e.APIError.ErrorObject.Errors),
)
}

// UnmarshalJSON satisfies encoding/json.Unmarshaler.
func (e *EventsAPIV2Error) UnmarshalJSON(data []byte) error {
var eo EventsAPIV2ErrorObject
if err := json.Unmarshal(data, &eo); err != nil {
return err
}

e.APIError.ErrorObject = eo
e.APIError.Valid = true

return nil
}

// BadRequest returns whether the event request was rejected by PagerDuty as an
// incorrect or invalid event structure.
func (e EventsAPIV2Error) BadRequest() bool {
return e.StatusCode == http.StatusBadRequest
}

// 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 (e EventsAPIV2Error) RateLimited() bool {
return e.StatusCode == http.StatusTooManyRequests
}

// APITimeout returns whether whether the response had a status of 408,
// indicating there was a request timeout on PagerDuty's side. This error is
// considered temporary, and so the request should be retried.
//
// Please note, this does not returnn true if the Go context.Context deadline
// was exceeded when making the request.
func (e EventsAPIV2Error) APITimeout() bool {
return e.StatusCode == http.StatusRequestTimeout
}

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

// NullEventsAPIV2ErrorObject is a wrapper around the EventsAPIV2ErrorObject type. If the Valid
// field is true, the API response included a structured error JSON object. This
// structured object is then set on the ErrorObject field.
//
// We assume it's possible in exceptional failure modes for error objects to be omitted by PagerDuty.
// As such, this wrapper type provides us a way to check if the object was
// provided while avoiding consumers accidentally missing a nil pointer check,
// thus crashing their whole program.
type NullEventsAPIV2ErrorObject struct {
Valid bool
ErrorObject EventsAPIV2ErrorObject
}

// EventsAPIV2ErrorObject represents the object returned by the Events V2 API when an error
// occurs. This includes messages that should hopefully provide useful context
// to the end user.
type EventsAPIV2ErrorObject struct {
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`

// Errors is likely to be empty, with the relevant error presented via the
// Status field instead.
Errors []string `json:"errors,omitempty"`
}

// ManageEventWithContext handles the trigger, acknowledge, and resolve methods for an event.
func ManageEventWithContext(ctx context.Context, e V2Event) (*V2EventResponse, error) {
data, err := json.Marshal(e)
Expand All @@ -73,21 +192,33 @@ func ManageEventWithContext(ctx context.Context, e V2Event) (*V2EventResponse, e
}

defer func() { _ = resp.Body.Close() }() // explicitly discard error

if resp.StatusCode != http.StatusAccepted {
bytes, err := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, EventsAPIV2Error{
StatusCode: resp.StatusCode,
message: fmt.Sprintf("HTTP response with status code: %d: error: %s", resp.StatusCode, err),
}
}
// now try to decode the response body into the error object.
var eae EventsAPIV2Error
err = json.Unmarshal(b, &eae)
if err != nil {
return nil, fmt.Errorf("HTTP Status Code: %d", resp.StatusCode)
eae = EventsAPIV2Error{
StatusCode: resp.StatusCode,
message: fmt.Sprintf("HTTP response with status code: %d, JSON unmarshal object body failed: %s, body: %s", resp.StatusCode, err, string(b)),
}
return nil, eae
}

return nil, fmt.Errorf("HTTP Status Code: %d, Message: %s", resp.StatusCode, string(bytes))
eae.StatusCode = resp.StatusCode
return nil, eae
}

var eventResponse V2EventResponse
if err := json.NewDecoder(resp.Body).Decode(&eventResponse); err != nil {
return nil, err
}

return &eventResponse, nil
}

Expand Down
Loading