Skip to content

Commit

Permalink
feat(error): show internal correlation id in error messages (#411)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apricote committed Apr 22, 2024
1 parent d4f67cb commit 6c96d19
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 0 deletions.
1 change: 1 addition & 0 deletions hcloud/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ func TestResourceActionClientAll(t *testing.T) {
}

func TestActionClientWatchOverallProgress(t *testing.T) {
t.Parallel()
env := newTestEnv()
defer env.Teardown()

Expand Down
10 changes: 10 additions & 0 deletions hcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,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
Expand Down Expand Up @@ -412,6 +416,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
Expand Down
7 changes: 7 additions & 0 deletions hcloud/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
33 changes: 33 additions & 0 deletions hcloud/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package hcloud

import (
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -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 {
Expand Down

0 comments on commit 6c96d19

Please sign in to comment.