-
Notifications
You must be signed in to change notification settings - Fork 244
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 EventsAPIV2Error from Events V2 API call. #368
Changes from all commits
d3e5bf6
1f6f620
b91cc0c
064618d
f01b045
f3fe7e5
3ac8942
e546488
4270270
96d05a3
6dd40ed
850c843
5adc5b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"` | ||
|
@@ -90,13 +90,15 @@ type APIErrorObject struct { | |
// While the PagerDuty REST API is documented to always return the error object, | ||
// we assume it's possible in exceptional failure modes for this to be omitted. | ||
// As such, this wrapper type provides us a way to check if the object was | ||
// provided while avoiding cosnumers accidentally missing a nil pointer check, | ||
// provided while avoiding consumers accidentally missing a nil pointer check, | ||
// thus crashing their whole program. | ||
type NullAPIErrorObject struct { | ||
Valid bool | ||
ErrorObject APIErrorObject | ||
} | ||
|
||
var _ json.Unmarshaler = &NullAPIErrorObject{} // assert that it satisfies the json.Unmarshaler interface. | ||
|
||
// UnmarshalJSON satisfies encoding/json.Unmarshaler | ||
func (n *NullAPIErrorObject) UnmarshalJSON(data []byte) error { | ||
var aeo APIErrorObject | ||
|
@@ -135,6 +137,8 @@ type APIError struct { | |
message string | ||
} | ||
|
||
var _ error = &APIError{} // assert that it implements the error interface. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like the other I commented on below I'm not sure there's much value in this specific interface check, could you remove it? |
||
|
||
// Error satisfies the error interface, and should contain the StatusCode, | ||
// APIErrorObject.Message, and APIErrorObject.Code. | ||
func (a APIError) Error() string { | ||
|
@@ -165,7 +169,7 @@ func (a APIError) Error() string { | |
func apiErrorsDetailString(errs []string) string { | ||
switch n := len(errs); n { | ||
case 0: | ||
panic("errs slice is empty") | ||
return "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be returned back to a |
||
|
||
case 1: | ||
return errs[0] | ||
|
@@ -319,7 +323,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. | ||
|
@@ -546,7 +550,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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though I personally agree with this change, could you undo it? I plan to lowercase all of the errors for v2, but the style of this library (within v1) is to capitalize the first word of an error message. |
||
} | ||
|
||
if resp.StatusCode < 200 || resp.StatusCode > 299 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,106 @@ 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:"-"` | ||
|
||
// EventsAPIV2Error 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 | ||
// EventsAPIV2Error will be false. If .Valid is true, the .ErrorObject field is | ||
// valid and should be consulted. | ||
EventsAPIV2Error NullEventsAPIV2ErrorObject | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you be willing to make this struct field |
||
|
||
message string | ||
} | ||
|
||
var _ error = &EventsAPIV2Error{} // assert that it satisfies the error interface. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this assertion is necessary as the compiler will flag it elsewhere. The |
||
var _ json.Unmarshaler = &EventsAPIV2Error{} // assert that it satisfies the json.Unmarshaler interface. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 here too, but also could you change this a little bit: var _ json.Unmarshaler = (*EventsAPIV2Error)(nil) |
||
|
||
// Error satisfies the error interface, and should contain the StatusCode, | ||
// EventsAPIV2Error.Message, EventsAPIV2Error.ErrorObject.Status, and EventsAPIV2Error.Errors. | ||
func (e EventsAPIV2Error) Error() string { | ||
if len(e.message) > 0 { | ||
return e.message | ||
} | ||
|
||
if !e.EventsAPIV2Error.Valid { | ||
return fmt.Sprintf("HTTP response failed with status code %d and no JSON error object was present", e.StatusCode) | ||
} | ||
|
||
return fmt.Sprintf( | ||
"HTTP response failed with status code: %d, message: %s, status: %s: %s", | ||
e.StatusCode, | ||
e.EventsAPIV2Error.ErrorObject.Message, | ||
e.EventsAPIV2Error.ErrorObject.Status, | ||
apiErrorsDetailString(e.EventsAPIV2Error.ErrorObject.Errors), | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the That doesn't seem like a very ideal error message, so could you handle the empty errors case separately to avoid the dangling |
||
} | ||
|
||
// UnmarshalJSON satisfies encoding/json.Unmarshaler. | ||
func (e *EventsAPIV2Error) UnmarshalJSON(data []byte) error { | ||
var eaeo EventsAPIV2ErrorObject | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOL. Say that five times fast... While you're in there making changes, I'm ok with just |
||
if err := json.Unmarshal(data, &eaeo); err != nil { | ||
return err | ||
} | ||
|
||
e.EventsAPIV2Error.ErrorObject = eaeo | ||
e.EventsAPIV2Error.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 | ||
} | ||
theckman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// RateLimited returns whether the response had e 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 | ||
} | ||
|
||
// Temporary returns whether it was a temporary error, one of which is a | ||
// RateLimited error. | ||
func (e EventsAPIV2Error) Temporary() bool { | ||
return e.RateLimited() || (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 []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) | ||
|
@@ -73,21 +173,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, fmt.Errorf("HTTP Status Code: %d", resp.StatusCode) | ||
return nil, EventsAPIV2Error{ | ||
StatusCode: resp.StatusCode, | ||
message: fmt.Sprintf("HTTP response with status code: %d: error: %s", resp.StatusCode, err), | ||
} | ||
abhishekrb19 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
// now try to decode the response body into the error object. | ||
var eae EventsAPIV2Error | ||
err = json.Unmarshal(b, &eae) | ||
if err != nil { | ||
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 | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍, but also could you change this a little bit: