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 {