Skip to content

Commit 6c96d19

Browse files
authored
feat(error): show internal correlation id in error messages (#411)
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.
1 parent d4f67cb commit 6c96d19

File tree

4 files changed

+51
-0
lines changed

4 files changed

+51
-0
lines changed

hcloud/action_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ func TestResourceActionClientAll(t *testing.T) {
322322
}
323323

324324
func TestActionClientWatchOverallProgress(t *testing.T) {
325+
t.Parallel()
325326
env := newTestEnv()
326327
defer env.Teardown()
327328

hcloud/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ func errorFromResponse(resp *Response, body []byte) error {
379379
return hcErr
380380
}
381381

382+
const (
383+
headerCorrelationID = "X-Correlation-Id"
384+
)
385+
382386
// Response represents a response from the API. It embeds http.Response.
383387
type Response struct {
384388
*http.Response
@@ -412,6 +416,12 @@ func (r *Response) readMeta(body []byte) error {
412416
return nil
413417
}
414418

419+
// internalCorrelationID returns the unique ID of the request as set by the API. This ID can help with support requests,
420+
// as it allows the people working on identify this request in particular.
421+
func (r *Response) internalCorrelationID() string {
422+
return r.Header.Get(headerCorrelationID)
423+
}
424+
415425
// Meta represents meta information included in an API response.
416426
type Meta struct {
417427
Pagination *Pagination

hcloud/error.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ type Error struct {
100100
}
101101

102102
func (e Error) Error() string {
103+
if resp := e.Response(); resp != nil {
104+
correlationID := resp.internalCorrelationID()
105+
if correlationID != "" {
106+
// For easier debugging, the error string contains the Correlation ID of the response.
107+
return fmt.Sprintf("%s (%s) (Correlation ID: %s)", e.Message, e.Code, correlationID)
108+
}
109+
}
103110
return fmt.Sprintf("%s (%s)", e.Message, e.Code)
104111
}
105112

hcloud/error_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package hcloud
22

33
import (
44
"fmt"
5+
"net/http"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -27,6 +28,38 @@ func TestError_Error(t *testing.T) {
2728
},
2829
want: "unable to authenticate (unauthorized)",
2930
},
31+
{
32+
name: "internal server error with correlation id",
33+
fields: fields{
34+
Code: ErrorCodeUnknownError,
35+
Message: "Creating image failed because of an unknown error.",
36+
response: &Response{
37+
Response: &http.Response{
38+
StatusCode: http.StatusInternalServerError,
39+
Header: func() http.Header {
40+
headers := http.Header{}
41+
// [http.Header] requires normalized header names, easiest to do by using the Set method
42+
headers.Set("X-Correlation-ID", "foobar")
43+
return headers
44+
}(),
45+
},
46+
},
47+
},
48+
want: "Creating image failed because of an unknown error. (unknown_error) (Correlation ID: foobar)",
49+
},
50+
{
51+
name: "internal server error without correlation id",
52+
fields: fields{
53+
Code: ErrorCodeUnknownError,
54+
Message: "Creating image failed because of an unknown error.",
55+
response: &Response{
56+
Response: &http.Response{
57+
StatusCode: http.StatusInternalServerError,
58+
},
59+
},
60+
},
61+
want: "Creating image failed because of an unknown error. (unknown_error)",
62+
},
3063
}
3164

3265
for _, tt := range tests {

0 commit comments

Comments
 (0)