Skip to content

Commit

Permalink
feat(error): show internal correlation id in error messages (#417)
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.

Backport 6c96d19 from #411.

Co-authored-by: Julian Tölle <julian.toelle@hetzner-cloud.de>
  • Loading branch information
github-actions[bot] and apricote committed Apr 22, 2024
1 parent 88ef99c commit f7f96a5
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
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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions hcloud/error.go
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
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 f7f96a5

Please sign in to comment.