diff --git a/internal/worker/clienterrors/errors.go b/internal/worker/clienterrors/errors.go index ed3884a28e..156ca87908 100644 --- a/internal/worker/clienterrors/errors.go +++ b/internal/worker/clienterrors/errors.go @@ -3,6 +3,7 @@ package clienterrors import ( "encoding/json" "fmt" + "reflect" ) const ( @@ -59,13 +60,57 @@ func (e *Error) String() string { return fmt.Sprintf("Code: %d, Reason: %s, Details: %v", e.ID, e.Reason, e.Details) } +func ensureNoErrs(v reflect.Value) error { + switch v.Kind() { + case reflect.Interface: + if errIf, ok := v.Interface().(error); ok { + if errIf != nil { + return fmt.Errorf("%v", errIf.Error()) + } + } + case reflect.Ptr: + if err := ensureNoErrs(v.Elem()); err != nil { + return err + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if err := ensureNoErrs(v.Field(i)); err != nil { + return err + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + if err := ensureNoErrs(v.Index(i)); err != nil { + return err + } + } + case reflect.Map: + for _, key := range v.MapKeys() { + if err := ensureNoErrs(v.MapIndex(key)); err != nil { + return err + } + } + } + return nil +} + func (e *Error) MarshalJSON() ([]byte, error) { var details interface{} switch v := e.Details.(type) { case error: details = v.Error() + case []error: + details = make([]string, 0, len(v)) + for _, err := range v { + if err != nil { + details = append(details.([]string), err.Error()) + } + } default: - details = v + if err := ensureNoErrs(reflect.ValueOf(v)); err != nil { + return nil, fmt.Errorf("found nested error in %+v: %v", e.Details, err) + } + details = e.Details } return json.Marshal(&struct { diff --git a/internal/worker/clienterrors/errors_test.go b/internal/worker/clienterrors/errors_test.go index 2e7eb9401a..1f986558d7 100644 --- a/internal/worker/clienterrors/errors_test.go +++ b/internal/worker/clienterrors/errors_test.go @@ -24,15 +24,42 @@ func TestErrorInterface(t *testing.T) { {fmt.Errorf("some error"), "some error"}, {&customErr{}, "customErr"}, } { - wce := clienterrors.WorkerClientError(2, "details", tc.err) - assert.Equal(t, fmt.Sprintf("Code: 2, Reason: details, Details: %s", tc.expectedStr), wce.String()) + wce := clienterrors.WorkerClientError(2, "reason", tc.err) + assert.Equal(t, fmt.Sprintf("Code: 2, Reason: reason, Details: %s", tc.expectedStr), wce.String()) } } func TestErrorJSONMarshal(t *testing.T) { - err := fmt.Errorf("some-error") + for _, tc := range []struct { + err interface{} + expectedStr string + }{ + {fmt.Errorf("some-error"), `"some-error"`}, + {[]error{fmt.Errorf("err1"), fmt.Errorf("err2")}, `["err1","err2"]`}, + {"random detail", `"random detail"`}, + } { + json, err := json.Marshal(clienterrors.WorkerClientError(2, "reason", tc.err)) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf(`{"id":2,"reason":"reason","details":%s}`, tc.expectedStr), string(json)) + } +} - json, err := json.Marshal(clienterrors.WorkerClientError(2, "details", err)) - assert.NoError(t, err) - assert.Equal(t, `{"id":2,"reason":"details","details":"some-error"}`, string(json)) +func TestErrorJSONMarshalDetectsNestedErrs(t *testing.T) { + details := struct { + Unrelated string + NestedErr error + Nested struct { + DeepErr error + } + }{ + Unrelated: "unrelated", + NestedErr: fmt.Errorf("some-nested-error"), + Nested: struct { + DeepErr error + }{ + DeepErr: fmt.Errorf("deep-err"), + }, + } + _, err := json.Marshal(clienterrors.WorkerClientError(2, "reason", details)) + assert.Equal(t, `json: error calling MarshalJSON for type *clienterrors.Error: found nested error in {Unrelated:unrelated NestedErr:some-nested-error Nested:{DeepErr:deep-err}}: some-nested-error`, err.Error()) }