From 2472e062cca0416da3da9467e0475ef7a526e41b Mon Sep 17 00:00:00 2001 From: Nicholas Ng Date: Tue, 29 Nov 2022 12:03:12 +0000 Subject: [PATCH] Marshal error with full context chain When using the new `Augment` function to add context and wrap errors, the error message in Go is chained and displayed with the full chain of context: ``` err := InternalService("code", "message", nil) augmentedErr := Augment(err, "context", nil) // This is "internal_service.code: context: message" augmentedErr.Error() ``` But the marshaled error only displays the first level of context, i.e. ``` // This is just "context" Marshal(augmentedErr.(*Error)).Message ``` This commit changes the Marshal behaviour to include the full context chain of error, so it's actually useful outside of Go when marshaled. ``` // This is now "context: message" Marshal(augmentedErr.(*Error)).Message ``` --- marshaling.go | 24 ++++++++++-- marshaling_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/marshaling.go b/marshaling.go index a0c2250..6b028b6 100644 --- a/marshaling.go +++ b/marshaling.go @@ -1,6 +1,8 @@ package terrors import ( + "strings" + pe "github.com/monzo/terrors/proto" "github.com/monzo/terrors/stack" ) @@ -15,14 +17,30 @@ func Marshal(e *Error) *pe.Error { } } + // Build message with all the context + errMessage := strings.Builder{} + errMessage.WriteString(e.Message) + next := e.cause + for next != nil { + errMessage.WriteString(": ") + switch typed := next.(type) { + case *Error: + errMessage.WriteString(typed.Message) + next = typed.cause + case error: + errMessage.WriteString(typed.Error()) + next = nil + } + } + retryable := &pe.BoolValue{} if e.IsRetryable != nil { retryable.Value = *e.IsRetryable } - err := &pe.Error{ + err := pe.Error{ Code: e.Code, - Message: e.Message, + Message: errMessage.String(), Stack: stackToProto(e.StackFrames), Params: e.Params, Retryable: retryable, @@ -30,7 +48,7 @@ func Marshal(e *Error) *pe.Error { if err.Code == "" { err.Code = ErrUnknown } - return err + return &err } // Unmarshal a protobuf error into a local error diff --git a/marshaling_test.go b/marshaling_test.go index 06d46a2..a63e9cc 100644 --- a/marshaling_test.go +++ b/marshaling_test.go @@ -1,6 +1,7 @@ package terrors import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -122,6 +123,61 @@ var marshalTestCases = []struct { Retryable: nil, }, }, + // Wrapped errors + { + Augment(&Error{ + Code: ErrInternalService, + Message: "bar", + }, "foo", nil).(*Error), + &pe.Error{ + Code: ErrInternalService, + Message: "foo: bar", + Retryable: nil, + Params: map[string]string{}, + }, + }, + { + Augment(&Error{ + Code: ErrInternalService, + Message: "bar", + }, "foo", map[string]string{"key": "value"}).(*Error), + &pe.Error{ + Code: ErrInternalService, + Message: "foo: bar", + Retryable: nil, + Params: map[string]string{"key": "value"}, + }, + }, + { + // Nested Augment + Augment( + Augment(&Error{ + Code: ErrInternalService, + Message: "baz", + }, + "bar", + map[string]string{"key": "value"}, + ), + "foo", + map[string]string{"key2": "value2"}, + ).(*Error), + &pe.Error{ + Code: ErrInternalService, + Message: "foo: bar: baz", + Retryable: nil, + Params: map[string]string{"key": "value", "key2": "value2"}, + }, + }, + { + // Wrapping a Go error + Augment(fmt.Errorf("a go error"), "boom", map[string]string{"key": "value"}).(*Error), + &pe.Error{ + Code: ErrInternalService, + Message: "boom: a go error", + Retryable: nil, + Params: map[string]string{"key": "value"}, + }, + }, } func TestMarshal(t *testing.T) { @@ -235,6 +291,42 @@ var unmarshalTestCases = []struct { Retryable: nil, }, }, + // Wrapped errors only gets unmarshaled as a single error + { + &Error{ + Code: ErrInternalService, + Message: "foo: bar: baz", // Augment(Augment(bazErr, "bar", nil), "foo", nil) + Params: map[string]string{}, + }, + &pe.Error{ + Code: ErrInternalService, + Message: "foo: bar: baz", + Retryable: &pe.BoolValue{ + Value: false, + }, + }, + }, + { + &Error{ + Code: ErrInternalService, + Message: "foo: bar", + Params: map[string]string{ + "key": "value", + "key2": "value2", + }, + }, + &pe.Error{ + Code: ErrInternalService, + Message: "foo: bar", + Retryable: &pe.BoolValue{ + Value: false, + }, + Params: map[string]string{ + "key": "value", + "key2": "value2", + }, + }, + }, } func TestUnmarshal(t *testing.T) {