From 95582395dfb1039f4ce4f10a1ac9c068db93a867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Fri, 20 Oct 2023 11:14:16 +0200 Subject: [PATCH] feat(error): include http response in api errors (#320) This allows us to show more useful information in debug logs in downstream projects. --------- Co-authored-by: Jonas Lammler --- hcloud/client.go | 11 +++++++---- hcloud/client_test.go | 3 +++ hcloud/error.go | 7 +++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/hcloud/client.go b/hcloud/client.go index f609ccfe..5be4c7b5 100644 --- a/hcloud/client.go +++ b/hcloud/client.go @@ -286,8 +286,8 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) { return response, fmt.Errorf("hcloud: error reading response meta data: %s", err) } - if resp.StatusCode >= 400 && resp.StatusCode <= 599 { - err = errorFromResponse(resp, body) + if response.StatusCode >= 400 && response.StatusCode <= 599 { + err = errorFromResponse(response, body) if err == nil { err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode) } else if IsError(err, ErrorCodeConflict) { @@ -359,7 +359,7 @@ func dumpRequest(r *http.Request) ([]byte, error) { return dumpReq, nil } -func errorFromResponse(resp *http.Response, body []byte) error { +func errorFromResponse(resp *Response, body []byte) error { if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { return nil } @@ -371,7 +371,10 @@ func errorFromResponse(resp *http.Response, body []byte) error { if respBody.Error.Code == "" && respBody.Error.Message == "" { return nil } - return ErrorFromSchema(respBody.Error) + + hcErr := ErrorFromSchema(respBody.Error) + hcErr.response = resp + return hcErr } // Response represents a response from the API. It embeds http.Response. diff --git a/hcloud/client_test.go b/hcloud/client_test.go index 36164cb7..49d20232 100644 --- a/hcloud/client_test.go +++ b/hcloud/client_test.go @@ -85,6 +85,9 @@ func TestClientError(t *testing.T) { if apiError.Message != "An error occurred" { t.Errorf("unexpected error message: %q", apiError.Message) } + if apiError.Response().StatusCode != http.StatusUnprocessableEntity { + t.Errorf("unexpected http status code: %q", apiError.Response().StatusCode) + } } func TestClientInvalidToken(t *testing.T) { diff --git a/hcloud/error.go b/hcloud/error.go index ac689d11..653043e6 100644 --- a/hcloud/error.go +++ b/hcloud/error.go @@ -94,12 +94,19 @@ type Error struct { Code ErrorCode Message string Details interface{} + + response *Response } func (e Error) Error() string { return fmt.Sprintf("%s (%s)", e.Message, e.Code) } +// Response returns the [Response] that contained the error if available. +func (e Error) Response() *Response { + return e.response +} + // ErrorDetailsInvalidInput contains the details of an 'invalid_input' error. type ErrorDetailsInvalidInput struct { Fields []ErrorDetailsInvalidInputField