Skip to content

Commit ac27789

Browse files
committed
support wrapError encode and decode
Previously, support for the `wrapError` type generated by `fmt.Errorf("%w xxxx")` was missing an understanding of the fact that the format string wasn't limited to just prefixes. Hence the `opaqueWrapper` struct assumed that all wrappers had prefix strings that would precede their causal chains. This change adds a flag to the `EncodedWrapper` which tracks whether the wrapper "owns" the error string. In the case of `fmt.Errorf` the `wrapError` struct created within the stdlib contains a fully interpolated string as its message, not the format string, or a prefix. This means that when we encode it into a `EncodedWrapper` for transfer over the network, we need to remember to just print the wrapper's string when `.Error()` is called on the decoded object on the other end. Paired with this change the `opaqueWrapper` that is generated on the decode side now contains this field as well which is referenced when calling `.Error()` on the wrapper, ensuring we don't re-concatenate the causes. This field is also referenced when formatting the error in `state.formatSingleLineOutput` where we use it to simply output the final entry's message, instead of concatenating all the entries together. This change is backwards compatible in that prior versions will not set the flag on the protobuf, and hence will continue to encode mis- configured `wrapError` instances that will see messages repeated when converted to Strings.
1 parent a95e895 commit ac27789

29 files changed

+589
-115
lines changed

contexttags/with_context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ func (w *withContext) SafeDetails() []string {
7575
return redactTags(w.tags)
7676
}
7777

78-
func encodeWithContext(_ context.Context, err error) (string, []string, proto.Message) {
78+
func encodeWithContext(_ context.Context, err error) (string, []string, proto.Message, bool) {
7979
w := err.(*withContext)
8080
p := &errorspb.TagsPayload{}
8181
for _, t := range w.tags.Get() {
8282
p.Tags = append(p.Tags, errorspb.TagPayload{Tag: t.Key(), Value: t.ValueStr()})
8383
}
84-
return "", w.SafeDetails(), p
84+
return "", w.SafeDetails(), p, false
8585
}
8686

8787
func decodeWithContext(

errbase/adapters.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,21 @@ func decodeWithMessage(
7676

7777
func encodePkgWithStack(
7878
_ context.Context, err error,
79-
) (msgPrefix string, safe []string, _ proto.Message) {
79+
) (msgPrefix string, safe []string, _ proto.Message, _ bool) {
8080
iErr := err.(interface{ StackTrace() pkgErr.StackTrace })
8181
safeDetails := []string{fmt.Sprintf("%+v", iErr.StackTrace())}
82-
return "" /* withStack does not have a message prefix */, safeDetails, nil
82+
return "" /* withStack does not have a message prefix */, safeDetails, nil, false
8383
}
8484

8585
func encodePathError(
8686
_ context.Context, err error,
87-
) (msgPrefix string, safe []string, details proto.Message) {
87+
) (msgPrefix string, safe []string, details proto.Message, ownError bool) {
8888
p := err.(*os.PathError)
8989
msg := p.Op + " " + p.Path
9090
details = &errorspb.StringsPayload{
9191
Details: []string{p.Op, p.Path},
9292
}
93-
return msg, []string{p.Op}, details
93+
return msg, []string{p.Op}, details, false
9494
}
9595

9696
func decodePathError(
@@ -113,13 +113,13 @@ func decodePathError(
113113

114114
func encodeLinkError(
115115
_ context.Context, err error,
116-
) (msgPrefix string, safe []string, details proto.Message) {
116+
) (msgPrefix string, safe []string, details proto.Message, ownError bool) {
117117
p := err.(*os.LinkError)
118118
msg := p.Op + " " + p.Old + " " + p.New
119119
details = &errorspb.StringsPayload{
120120
Details: []string{p.Op, p.Old, p.New},
121121
}
122-
return msg, []string{p.Op}, details
122+
return msg, []string{p.Op}, details, false
123123
}
124124

125125
func decodeLinkError(
@@ -143,9 +143,9 @@ func decodeLinkError(
143143

144144
func encodeSyscallError(
145145
_ context.Context, err error,
146-
) (msgPrefix string, safe []string, details proto.Message) {
146+
) (msgPrefix string, safe []string, details proto.Message, ownError bool) {
147147
p := err.(*os.SyscallError)
148-
return p.Syscall, nil, nil
148+
return p.Syscall, nil, nil, false
149149
}
150150

151151
func decodeSyscallError(
@@ -211,4 +211,12 @@ func init() {
211211
RegisterWrapperDecoder(pKey, decodeSyscallError)
212212

213213
RegisterLeafEncoder(GetTypeKey(&OpaqueErrno{}), encodeOpaqueErrno)
214+
215+
RegisterWrapperEncoder(GetTypeKey(fmt.Errorf("text %w text", baseErr)), encodeWrapError)
216+
}
217+
218+
func encodeWrapError(_ context.Context, err error) (
219+
msgPrefix string, safeDetails []string, payload proto.Message, ownError bool,
220+
) {
221+
return err.Error(), nil, nil, true
214222
}

errbase/adapters_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ func TestAdaptBaseGoErr(t *testing.T) {
5555
tt.CheckDeepEqual(newErr, origErr)
5656
}
5757

58+
func TestAdaptGoSingleWrapErr(t *testing.T) {
59+
origErr := fmt.Errorf("an error %w", goErr.New("hello"))
60+
t.Logf("start err: %# v", pretty.Formatter(origErr))
61+
62+
newErr := network(t, origErr)
63+
64+
tt := testutils.T{T: t}
65+
// The library preserves the cause. It's not possible to preserve the fmt
66+
// string.
67+
tt.CheckContains(newErr.Error(), "hello")
68+
}
69+
5870
func TestAdaptPkgWithMessage(t *testing.T) {
5971
// Simple message wrappers from github.com/pkg/errors are preserved
6072
// completely.

errbase/decode.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ func decodeWrapper(ctx context.Context, enc *errorspb.EncodedWrapper) error {
107107

108108
// Otherwise, preserve all details about the original object.
109109
return &opaqueWrapper{
110-
cause: cause,
111-
prefix: enc.MessagePrefix,
112-
details: enc.Details,
110+
cause: cause,
111+
prefix: enc.MessagePrefix,
112+
details: enc.Details,
113+
ownsErrorString: enc.WrapperOwnsErrorString,
113114
}
114115
}
115116

errbase/encode.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func encodeAsAny(ctx context.Context, err error, payload proto.Message) *types.A
115115
func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
116116
var msg string
117117
var details errorspb.EncodedErrorDetails
118+
var ownError bool
118119

119120
if e, ok := err.(*opaqueWrapper); ok {
120121
msg = e.prefix
@@ -127,7 +128,7 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
127128
// If we have a manually registered encoder, use that.
128129
typeKey := TypeKey(details.ErrorTypeMark.FamilyName)
129130
if enc, ok := encoders[typeKey]; ok {
130-
msg, details.ReportablePayload, payload = enc(ctx, err)
131+
msg, details.ReportablePayload, payload, ownError = enc(ctx, err)
131132
} else {
132133
// No encoder.
133134
// In that case, we'll try to compute a message prefix
@@ -148,19 +149,22 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
148149
return EncodedError{
149150
Error: &errorspb.EncodedError_Wrapper{
150151
Wrapper: &errorspb.EncodedWrapper{
151-
Cause: EncodeError(ctx, cause),
152-
MessagePrefix: msg,
153-
Details: details,
152+
Cause: EncodeError(ctx, cause),
153+
MessagePrefix: msg,
154+
Details: details,
155+
WrapperOwnsErrorString: ownError,
154156
},
155157
},
156158
}
157159
}
158160

159161
// extractPrefix extracts the prefix from a wrapper's error message.
160162
// For example,
161-
// err := errors.New("bar")
162-
// err = errors.Wrap(err, "foo")
163-
// extractPrefix(err)
163+
//
164+
// err := errors.New("bar")
165+
// err = errors.Wrap(err, "foo")
166+
// extractPrefix(err)
167+
//
164168
// returns "foo".
165169
func extractPrefix(err, cause error) string {
166170
causeSuffix := cause.Error()
@@ -304,7 +308,7 @@ func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
304308

305309
// WrapperEncoder is to be provided (via RegisterWrapperEncoder above)
306310
// by additional wrapper types not yet known to this library.
307-
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
311+
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, ownError bool)
308312

309313
// registry for RegisterWrapperType.
310314
var encoders = map[TypeKey]WrapperEncoder{}

errbase/format_error.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ func formatErrorInternal(err error, s fmt.State, verb rune, redactableOutput boo
147147
// recursion. So we have no choice but to peel the data
148148
// and then assemble the pieces ourselves.
149149
p.formatRecursive(err, true /* isOutermost */, false /* withDetail */)
150-
p.formatSingleLineOutput()
150+
if ee, ok := err.(*opaqueWrapper); ok {
151+
p.formatSingleLineOutput(ee.ownsErrorString)
152+
} else {
153+
p.formatSingleLineOutput(false)
154+
}
151155
p.finishDisplay(verb)
152156

153157
default:
@@ -185,7 +189,7 @@ func (s *state) formatEntries(err error) {
185189
//
186190
// <complete error message>
187191
// (1) <details>
188-
s.formatSingleLineOutput()
192+
s.formatSingleLineOutput(false)
189193
s.finalBuf.WriteString("\n(1)")
190194

191195
s.printEntry(s.entries[len(s.entries)-1])
@@ -278,8 +282,9 @@ func (s *state) printEntry(entry formatEntry) {
278282
//
279283
// This function is used both when FormatError() is called indirectly
280284
// from .Error(), e.g. in:
281-
// (e *myType) Error() { return fmt.Sprintf("%v", e) }
282-
// (e *myType) Format(s fmt.State, verb rune) { errors.FormatError(s, verb, e) }
285+
//
286+
// (e *myType) Error() { return fmt.Sprintf("%v", e) }
287+
// (e *myType) Format(s fmt.State, verb rune) { errors.FormatError(s, verb, e) }
283288
//
284289
// and also to print the first line in the output of a %+v format.
285290
//
@@ -290,7 +295,7 @@ func (s *state) printEntry(entry formatEntry) {
290295
// RedactableBytes. However, we are not using the helper facilities
291296
// from redact.SafePrinter to do this, so care should be taken below
292297
// to properly escape markers, etc.
293-
func (s *state) formatSingleLineOutput() {
298+
func (s *state) formatSingleLineOutput(parentOwnsErrorString bool) {
294299
for i := len(s.entries) - 1; i >= 0; i-- {
295300
entry := &s.entries[i]
296301
if entry.elideShort {
@@ -316,6 +321,10 @@ func (s *state) formatSingleLineOutput() {
316321
// This means entry.head is unsafe. We need to escape it.
317322
s.finalBuf.Write([]byte(redact.EscapeBytes(entry.head)))
318323
}
324+
if parentOwnsErrorString {
325+
// only process the final wrapper
326+
return
327+
}
319328
}
320329
}
321330

errbase/format_error_internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestFormatSingleLineOutput(t *testing.T) {
6565

6666
for _, tc := range testCases {
6767
s := state{entries: tc.entries}
68-
s.formatSingleLineOutput()
68+
s.formatSingleLineOutput(false)
6969
if s.finalBuf.String() != tc.exp {
7070
t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String())
7171
}
@@ -165,7 +165,7 @@ func TestFormatSingleLineOutputRedactable(t *testing.T) {
165165

166166
for _, tc := range testCases {
167167
s := state{entries: tc.entries, redactableOutput: true}
168-
s.formatSingleLineOutput()
168+
s.formatSingleLineOutput(false)
169169
if s.finalBuf.String() != tc.exp {
170170
t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String())
171171
}

errbase/opaque.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ var _ SafeFormatter = (*opaqueLeaf)(nil)
4040
// back to some network system that _does_ know about
4141
// the type, the original object can be restored.
4242
type opaqueWrapper struct {
43-
cause error
44-
prefix string
45-
details errorspb.EncodedErrorDetails
43+
cause error
44+
prefix string
45+
details errorspb.EncodedErrorDetails
46+
ownsErrorString bool
4647
}
4748

4849
var _ error = (*opaqueWrapper)(nil)
@@ -53,6 +54,9 @@ var _ SafeFormatter = (*opaqueWrapper)(nil)
5354
func (e *opaqueLeaf) Error() string { return e.msg }
5455

5556
func (e *opaqueWrapper) Error() string {
57+
if e.ownsErrorString {
58+
return e.prefix
59+
}
5660
if e.prefix == "" {
5761
return e.cause.Error()
5862
}

errbase/unknown_type_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ func TestUnknownWrapperTraversal(t *testing.T) {
162162
t.Logf("start err: %# v", pretty.Formatter(origErr))
163163

164164
// Register a temporary encoder.
165-
myEncode := func(_ context.Context, err error) (string, []string, proto.Message) {
165+
myEncode := func(_ context.Context, err error) (string, []string, proto.Message, bool) {
166166
m := err.(*myWrap)
167-
return "", nil, &internal.MyPayload{Val: int32(m.val)}
167+
return "", nil, &internal.MyPayload{Val: int32(m.val)}, false
168168
}
169169
tn := errbase.GetTypeKey((*myWrap)(nil))
170170
errbase.RegisterWrapperEncoder(tn, myEncode)

0 commit comments

Comments
 (0)