From f7f96a5d5ebc0d8239b946b928d5bd534cc2bf21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:10:04 +0200 Subject: [PATCH] feat(error): show internal correlation id in error messages (#417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes the way error responses from the API are formatted for display to users. When available, it adds the Correlation (Trace) ID header to the error string. We often have users that post the error string, but rarely do users have debug logging active. By adding the correlation ID to error messages, we can more quickly investigate why something went wrong using the Hetzner internal tracing system. This is common practice in Web Apps, they usually show a request/trace ID or an encrypted blob in case of an internal server error. Backport 6c96d19dacde736b52abd7b8fc8879c8f721f23b from #411. Co-authored-by: Julian Tölle --- hcloud/action_test.go | 1 + hcloud/client.go | 10 ++++++++++ hcloud/error.go | 7 +++++++ hcloud/error_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/hcloud/action_test.go b/hcloud/action_test.go index 113019ea..275ea5b4 100644 --- a/hcloud/action_test.go +++ b/hcloud/action_test.go @@ -322,6 +322,7 @@ func TestResourceActionClientAll(t *testing.T) { } func TestActionClientWatchOverallProgress(t *testing.T) { + t.Parallel() env := newTestEnv() defer env.Teardown() diff --git a/hcloud/client.go b/hcloud/client.go index 570066ac..5558e3c6 100644 --- a/hcloud/client.go +++ b/hcloud/client.go @@ -377,6 +377,10 @@ func errorFromResponse(resp *Response, body []byte) error { return hcErr } +const ( + headerCorrelationID = "X-Correlation-Id" +) + // Response represents a response from the API. It embeds http.Response. type Response struct { *http.Response @@ -410,6 +414,12 @@ func (r *Response) readMeta(body []byte) error { return nil } +// internalCorrelationID returns the unique ID of the request as set by the API. This ID can help with support requests, +// as it allows the people working on identify this request in particular. +func (r *Response) internalCorrelationID() string { + return r.Header.Get(headerCorrelationID) +} + // Meta represents meta information included in an API response. type Meta struct { Pagination *Pagination diff --git a/hcloud/error.go b/hcloud/error.go index 2b5c4930..7403e420 100644 --- a/hcloud/error.go +++ b/hcloud/error.go @@ -100,6 +100,13 @@ type Error struct { } func (e Error) Error() string { + if resp := e.Response(); resp != nil { + correlationID := resp.internalCorrelationID() + if correlationID != "" { + // For easier debugging, the error string contains the Correlation ID of the response. + return fmt.Sprintf("%s (%s) (Correlation ID: %s)", e.Message, e.Code, correlationID) + } + } return fmt.Sprintf("%s (%s)", e.Message, e.Code) } diff --git a/hcloud/error_test.go b/hcloud/error_test.go index 300added..c4816110 100644 --- a/hcloud/error_test.go +++ b/hcloud/error_test.go @@ -2,6 +2,7 @@ package hcloud import ( "fmt" + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -27,6 +28,38 @@ func TestError_Error(t *testing.T) { }, want: "unable to authenticate (unauthorized)", }, + { + name: "internal server error with correlation id", + fields: fields{ + Code: ErrorCodeUnknownError, + Message: "Creating image failed because of an unknown error.", + response: &Response{ + Response: &http.Response{ + StatusCode: http.StatusInternalServerError, + Header: func() http.Header { + headers := http.Header{} + // [http.Header] requires normalized header names, easiest to do by using the Set method + headers.Set("X-Correlation-ID", "foobar") + return headers + }(), + }, + }, + }, + want: "Creating image failed because of an unknown error. (unknown_error) (Correlation ID: foobar)", + }, + { + name: "internal server error without correlation id", + fields: fields{ + Code: ErrorCodeUnknownError, + Message: "Creating image failed because of an unknown error.", + response: &Response{ + Response: &http.Response{ + StatusCode: http.StatusInternalServerError, + }, + }, + }, + want: "Creating image failed because of an unknown error. (unknown_error)", + }, } for _, tt := range tests {