Skip to content

Commit e3c1d21

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 since older versions will omit the new field in the proto, defaulting it to `false` in newer versions, which will preserve existing behavior when printing out errors. New code will set the field when necessary and interpret it appropriately. If an error encoded with a new version is decoded by an older version, the information about the error string will be discarded, which may lead to repeated portions of the error message when printed out, if the wrapper contains a copy of the message of its cause.
1 parent a95e895 commit e3c1d21

30 files changed

+7656
-511
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: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,18 @@ 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 {
121+
// We delegate all knowledge of the error string
122+
// to the original encoder and do not try to re-engineer
123+
// the prefix out of the error. This helps maintain
124+
// backward compatibility with earlier versions of the
125+
// encoder which don't have any understanding of
126+
// error string ownership by the wrapper.
120127
msg = e.prefix
121128
details = e.details
129+
ownError = e.ownsErrorString
122130
} else {
123131
details.OriginalTypeName, details.ErrorTypeMark.FamilyName, details.ErrorTypeMark.Extension = getTypeDetails(err, false /*onlyFamily*/)
124132

@@ -127,12 +135,12 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
127135
// If we have a manually registered encoder, use that.
128136
typeKey := TypeKey(details.ErrorTypeMark.FamilyName)
129137
if enc, ok := encoders[typeKey]; ok {
130-
msg, details.ReportablePayload, payload = enc(ctx, err)
138+
msg, details.ReportablePayload, payload, ownError = enc(ctx, err)
131139
} else {
132140
// No encoder.
133141
// In that case, we'll try to compute a message prefix
134142
// manually.
135-
msg = extractPrefix(err, cause)
143+
msg, ownError = extractPrefix(err, cause)
136144

137145
// If there are known safe details, use them.
138146
if s, ok := err.(SafeDetailer); ok {
@@ -148,31 +156,47 @@ func encodeWrapper(ctx context.Context, err, cause error) EncodedError {
148156
return EncodedError{
149157
Error: &errorspb.EncodedError_Wrapper{
150158
Wrapper: &errorspb.EncodedWrapper{
151-
Cause: EncodeError(ctx, cause),
152-
MessagePrefix: msg,
153-
Details: details,
159+
Cause: EncodeError(ctx, cause),
160+
MessagePrefix: msg,
161+
Details: details,
162+
WrapperOwnsErrorString: ownError,
154163
},
155164
},
156165
}
157166
}
158167

159168
// extractPrefix extracts the prefix from a wrapper's error message.
160169
// For example,
161-
// err := errors.New("bar")
162-
// err = errors.Wrap(err, "foo")
163-
// extractPrefix(err)
170+
//
171+
// err := errors.New("bar")
172+
// err = errors.Wrap(err, "foo")
173+
// extractPrefix(err)
174+
//
164175
// returns "foo".
165-
func extractPrefix(err, cause error) string {
176+
//
177+
// If a presumed wrapper does not have a message prefix, it is assumed
178+
// to override the entire error message and `extractPrefix` returns
179+
// the entire message and the boolean `true` to signify that the causes
180+
// should not be appended to it.
181+
func extractPrefix(err, cause error) (string, bool) {
166182
causeSuffix := cause.Error()
167183
errMsg := err.Error()
168184

169185
if strings.HasSuffix(errMsg, causeSuffix) {
170186
prefix := errMsg[:len(errMsg)-len(causeSuffix)]
187+
// If error msg matches exactly then this is a wrapper
188+
// with no message of its own.
189+
if len(prefix) == 0 {
190+
return "", false
191+
}
171192
if strings.HasSuffix(prefix, ": ") {
172-
return prefix[:len(prefix)-2]
193+
return prefix[:len(prefix)-2], false
173194
}
174195
}
175-
return ""
196+
// If we don't have the cause as a suffix, then we have
197+
// some other string as our error msg, preserve that and
198+
// mark as override
199+
return errMsg, true
176200
}
177201

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

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

errbase/err_string_ownership_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package errbase_test
2+
3+
import (
4+
"context"
5+
goErr "errors"
6+
"fmt"
7+
"testing"
8+
9+
"github.com/cockroachdb/errors/errbase"
10+
"github.com/cockroachdb/errors/errorspb"
11+
"github.com/cockroachdb/errors/testutils"
12+
)
13+
14+
func genEncoded(ownsErrorString bool) errorspb.EncodedError {
15+
return errorspb.EncodedError{
16+
Error: &errorspb.EncodedError_Wrapper{
17+
Wrapper: &errorspb.EncodedWrapper{
18+
Cause: errorspb.EncodedError{
19+
Error: &errorspb.EncodedError_Leaf{
20+
Leaf: &errorspb.EncodedErrorLeaf{
21+
Message: "leaf-error-msg",
22+
},
23+
},
24+
},
25+
MessagePrefix: "wrapper-error-msg: leaf-error-msg: extra info",
26+
Details: errorspb.EncodedErrorDetails{},
27+
WrapperOwnsErrorString: ownsErrorString,
28+
},
29+
},
30+
}
31+
}
32+
33+
func TestDecodeOldVersion(t *testing.T) {
34+
tt := testutils.T{T: t}
35+
36+
errOldEncoded := genEncoded(false)
37+
errOldDecoded := errbase.DecodeError(context.Background(), errOldEncoded)
38+
// Ensure that we will continue to just concat leaf after wrapper
39+
// with older errors for backward compatibility.
40+
tt.CheckEqual(errOldDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info: leaf-error-msg")
41+
42+
// Check to ensure that when flag is present, we interpret things correctly.
43+
errNewEncoded := genEncoded(true)
44+
errNewDecoded := errbase.DecodeError(context.Background(), errNewEncoded)
45+
tt.CheckEqual(errNewDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info")
46+
}
47+
48+
func TestEncodeDecodeNewVersion(t *testing.T) {
49+
tt := testutils.T{T: t}
50+
errNewEncoded := errbase.EncodeError(
51+
context.Background(),
52+
fmt.Errorf(
53+
"wrapper-error-msg: %w: extra info",
54+
goErr.New("leaf-error-msg"),
55+
),
56+
)
57+
58+
errNew := errorspb.EncodedError{
59+
Error: &errorspb.EncodedError_Wrapper{
60+
Wrapper: &errorspb.EncodedWrapper{
61+
Cause: errorspb.EncodedError{
62+
Error: &errorspb.EncodedError_Leaf{
63+
Leaf: &errorspb.EncodedErrorLeaf{
64+
Message: "leaf-error-msg",
65+
Details: errorspb.EncodedErrorDetails{
66+
OriginalTypeName: "errors/*errors.errorString",
67+
ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "errors/*errors.errorString", Extension: ""},
68+
ReportablePayload: nil,
69+
FullDetails: nil,
70+
},
71+
},
72+
},
73+
},
74+
MessagePrefix: "wrapper-error-msg: leaf-error-msg: extra info",
75+
Details: errorspb.EncodedErrorDetails{
76+
OriginalTypeName: "fmt/*fmt.wrapError",
77+
ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "fmt/*fmt.wrapError", Extension: ""},
78+
ReportablePayload: nil,
79+
FullDetails: nil,
80+
},
81+
WrapperOwnsErrorString: true,
82+
},
83+
},
84+
}
85+
86+
tt.CheckDeepEqual(errNewEncoded, errNew)
87+
newErr := errbase.DecodeError(context.Background(), errNew)
88+
89+
// New version correctly decodes error
90+
tt.CheckEqual(newErr.Error(), "wrapper-error-msg: leaf-error-msg: extra info")
91+
}

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.

0 commit comments

Comments
 (0)