Skip to content

Commit 69e17d2

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 69e17d2

29 files changed

+7196
-515
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,11 @@ func RegisterLeafDecoder(typeName TypeKey, decoder LeafDecoder)
573573
func RegisterLeafEncoder(typeName TypeKey, encoder LeafEncoder)
574574
func RegisterWrapperDecoder(typeName TypeKey, decoder WrapperDecoder)
575575
func RegisterWrapperEncoder(typeName TypeKey, encoder WrapperEncoder)
576+
func RegisterWrapperEncoderWithMessageOverride (typeName TypeKey, encoder WrapperEncoderWithMessageOverride)
576577
type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
577578
type LeafDecoder = func(ctx context.Context, msg string, safeDetails []string, payload proto.Message) error
578579
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
580+
type WrapperEncoderWithMessageOverride = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool)
579581
type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, safeDetails []string, payload proto.Message) error
580582
581583
// Registering package renames for custom error types.

errbase/adapters.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,9 @@ func init() {
212212

213213
RegisterLeafEncoder(GetTypeKey(&OpaqueErrno{}), encodeOpaqueErrno)
214214
}
215+
216+
func encodeWrapError(_ context.Context, err error) (
217+
msgPrefix string, safeDetails []string, payload proto.Message, ownError bool,
218+
) {
219+
return err.Error(), nil, nil, true
220+
}

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: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,12 @@ 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 {
120-
msg = e.prefix
121+
prefix, ownErrorPrefix := extractPrefix(err, cause)
122+
msg = prefix
123+
ownError = e.ownsErrorString || ownErrorPrefix
121124
details = e.details
122125
} else {
123126
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
@@ -127,12 +130,12 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
127130
// If we have a manually registered encoder, use that.
128131
typeKey := TypeKey(details.ErrorTypeMark.FamilyName)
129132
if enc, ok := encoders[typeKey]; ok {
130-
msg, details.ReportablePayload, payload = enc(ctx, err)
133+
msg, details.ReportablePayload, payload, ownError = enc(ctx, err)
131134
} else {
132135
// No encoder.
133136
// In that case, we'll try to compute a message prefix
134137
// manually.
135-
msg = extractPrefix(err, cause)
138+
msg, ownError = extractPrefix(err, cause)
136139

137140
// If there are known safe details, use them.
138141
if s, ok := err.(SafeDetailer); ok {
@@ -148,31 +151,47 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
148151
return EncodedError{
149152
Error: &errorspb.EncodedError_Wrapper{
150153
Wrapper: &errorspb.EncodedWrapper{
151-
Cause: EncodeError(ctx, cause),
152-
MessagePrefix: msg,
153-
Details: details,
154+
Cause: EncodeError(ctx, cause),
155+
MessagePrefix: msg,
156+
Details: details,
157+
WrapperOwnsErrorString: ownError,
154158
},
155159
},
156160
}
157161
}
158162

159163
// extractPrefix extracts the prefix from a wrapper's error message.
160164
// For example,
161-
// err := errors.New("bar")
162-
// err = errors.Wrap(err, "foo")
163-
// extractPrefix(err)
165+
//
166+
// err := errors.New("bar")
167+
// err = errors.Wrap(err, "foo")
168+
// extractPrefix(err)
169+
//
164170
// returns "foo".
165-
func extractPrefix(err, cause error) string {
171+
//
172+
// If a presumed wrapper does not have a message prefix, it is assumed
173+
// to override the entire error message and `extractPrefix` returns
174+
// the entire message and the boolean `true` to signify that the causes
175+
// should not be appended to it.
176+
func extractPrefix(err, cause error) (string, bool) {
166177
causeSuffix := cause.Error()
167178
errMsg := err.Error()
168179

169180
if strings.HasSuffix(errMsg, causeSuffix) {
170181
prefix := errMsg[:len(errMsg)-len(causeSuffix)]
182+
// If error msg matches exactly then this is a wrapper
183+
// with no message of its own.
184+
if len(prefix) == 0 {
185+
return "", false
186+
}
171187
if strings.HasSuffix(prefix, ": ") {
172-
return prefix[:len(prefix)-2]
188+
return prefix[:len(prefix)-2], false
173189
}
174190
}
175-
return ""
191+
// If we don't have the cause as a suffix, then we have
192+
// some other string as our error msg, preserve that and
193+
// mark as override
194+
return errMsg, true
176195
}
177196

178197
func getTypeDetails(
@@ -295,6 +314,26 @@ var leafEncoders = map[TypeKey]LeafEncoder{}
295314
// or a different type, ensure that RegisterTypeMigration() was called
296315
// prior to RegisterWrapperEncoder().
297316
func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
317+
RegisterWrapperEncoderWithMessageOverride(theType, func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool) {
318+
prefix, details, payload := encoder(ctx, err)
319+
return prefix, details, payload, false
320+
})
321+
}
322+
323+
// RegisterWrapperEncoderWithMessageOverride can be used to register
324+
// new wrapper types to the library. Registered wrappers will be
325+
// encoded using their own Go type when an error is encoded. Wrappers
326+
// that have not been registered will be encoded using the
327+
// opaqueWrapper type.
328+
//
329+
// This function differs from RegisterWrapperEncoder by allowing the
330+
// caller to explicitly decide whether the wrapper owns the entire
331+
// error message or not. Otherwise, the relationship is inferred.
332+
//
333+
// Note: if the error type has been migrated from a previous location
334+
// or a different type, ensure that RegisterTypeMigration() was called
335+
// prior to RegisterWrapperEncoder().
336+
func RegisterWrapperEncoderWithMessageOverride(theType TypeKey, encoder WrapperEncoderWithMessageOverride) {
298337
if encoder == nil {
299338
delete(encoders, theType)
300339
} else {
@@ -306,5 +345,12 @@ func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
306345
// by additional wrapper types not yet known to this library.
307346
type WrapperEncoder = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message)
308347

348+
// WrapperEncoderWithMessageOverride is to be provided (via RegisterWrapperEncoderWithMessageOverride above)
349+
// by additional wrapper types not yet known to this library.
350+
// This encoder accepts an additional boolean flag which determines whether the
351+
// wrapper owns the error message completely instead of simply being a prefix
352+
// with the error message of its causes appended to it.
353+
type WrapperEncoderWithMessageOverride = func(ctx context.Context, err error) (msgPrefix string, safeDetails []string, payload proto.Message, overrideError bool)
354+
309355
// registry for RegisterWrapperType.
310-
var encoders = map[TypeKey]WrapperEncoder{}
356+
var encoders = map[TypeKey]WrapperEncoderWithMessageOverride{}

errbase/format_error.go

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,9 @@ func (s *state) printEntry(entry formatEntry) {
278278
//
279279
// This function is used both when FormatError() is called indirectly
280280
// 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) }
281+
//
282+
// (e *myType) Error() { return fmt.Sprintf("%v", e) }
283+
// (e *myType) Format(s fmt.State, verb rune) { errors.FormatError(s, verb, e) }
283284
//
284285
// and also to print the first line in the output of a %+v format.
285286
//
@@ -356,19 +357,15 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
356357
if desiredShortening == nil {
357358
// The error wants to elide the short messages from inner
358359
// causes. Do it.
359-
for i := range s.entries {
360-
s.entries[i].elideShort = true
361-
}
360+
s.elideFurtherCauseMsgs()
362361
}
363362

364363
case Formatter:
365364
desiredShortening := v.FormatError((*printer)(s))
366365
if desiredShortening == nil {
367366
// The error wants to elide the short messages from inner
368367
// causes. Do it.
369-
for i := range s.entries {
370-
s.entries[i].elideShort = true
371-
}
368+
s.elideFurtherCauseMsgs()
372369
}
373370

374371
case fmt.Formatter:
@@ -389,7 +386,11 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
389386
s.lastStack = st.StackTrace()
390387
}
391388
} else {
392-
s.formatSimple(err, cause)
389+
if elideCauseMsg := s.formatSimple(err, cause); elideCauseMsg {
390+
// The error wants to elide the short messages from inner
391+
// causes. Do it.
392+
s.elideFurtherCauseMsgs()
393+
}
393394
}
394395

395396
default:
@@ -411,9 +412,7 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
411412
if desiredShortening == nil {
412413
// The error wants to elide the short messages from inner
413414
// causes. Do it.
414-
for i := range s.entries {
415-
s.entries[i].elideShort = true
416-
}
415+
s.elideFurtherCauseMsgs()
417416
}
418417
break
419418
}
@@ -422,7 +421,11 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
422421
// If the error did not implement errors.Formatter nor
423422
// fmt.Formatter, but it is a wrapper, still attempt best effort:
424423
// print what we can at this level.
425-
s.formatSimple(err, cause)
424+
if elideCauseMsg := s.formatSimple(err, cause); elideCauseMsg {
425+
// The error wants to elide the short messages from inner
426+
// causes. Do it.
427+
s.elideFurtherCauseMsgs()
428+
}
426429
}
427430
}
428431

@@ -443,6 +446,18 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
443446
s.buf = bytes.Buffer{}
444447
}
445448

449+
// elideFurtherCauseMsgs sets the `elideShort` field
450+
// on all entries added so far to `true`. Because these
451+
// entries are added recursively from the innermost
452+
// cause outward, we can iterate through all entries
453+
// without bound because the caller is guaranteed not
454+
// to see entries that it is the causer of.
455+
func (s *state) elideFurtherCauseMsgs() {
456+
for i := range s.entries {
457+
s.entries[i].elideShort = true
458+
}
459+
}
460+
446461
func (s *state) collectEntry(err error, bufIsRedactable bool) formatEntry {
447462
entry := formatEntry{err: err}
448463
if s.wantDetail {
@@ -500,16 +515,19 @@ func RegisterSpecialCasePrinter(fn safeErrorPrinterFn) {
500515
// formatSimple performs a best effort at extracting the details at a
501516
// given level of wrapping when the error object does not implement
502517
// the Formatter interface.
503-
func (s *state) formatSimple(err, cause error) {
518+
// Returns true if we want to elide errors from causal chain.
519+
func (s *state) formatSimple(err, cause error) bool {
504520
var pref string
521+
elideCauses := false
505522
if cause != nil {
506-
pref = extractPrefix(err, cause)
523+
pref, elideCauses = extractPrefix(err, cause)
507524
} else {
508525
pref = err.Error()
509526
}
510527
if len(pref) > 0 {
511528
s.Write([]byte(pref))
512529
}
530+
return elideCauses
513531
}
514532

515533
// finishDisplay renders s.finalBuf into s.State.

errbase/format_simple.go

Lines changed: 0 additions & 36 deletions
This file was deleted.

errbase/opaque.go

Lines changed: 10 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
}
@@ -103,5 +107,8 @@ func (e *opaqueWrapper) SafeFormatError(p Printer) (next error) {
103107
p.Printf("\npayload type: %s", redact.Safe(e.details.FullDetails.TypeUrl))
104108
}
105109
}
110+
if e.ownsErrorString {
111+
return nil
112+
}
106113
return e.cause
107114
}

0 commit comments

Comments
 (0)