Skip to content

Commit 19be170

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 an enum to the `EncodedWrapper` which tracks whether the wrapper's error string is a prefix, or the full error. 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 `PREFIX` 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 1456059 commit 19be170

29 files changed

+7416
-532
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_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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func decodeWrapper(ctx context.Context, enc *errorspb.EncodedWrapper) error {
9797
typeKey := TypeKey(enc.Details.ErrorTypeMark.FamilyName)
9898
if decoder, ok := decoders[typeKey]; ok {
9999
// Yes, use it.
100-
genErr := decoder(ctx, cause, enc.MessagePrefix, enc.Details.ReportablePayload, payload)
100+
genErr := decoder(ctx, cause, enc.Message, enc.Details.ReportablePayload, payload)
101101
if genErr != nil {
102102
// Decoding succeeded. Use this.
103103
return genErr
@@ -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.Message,
112+
details: enc.Details,
113+
messageType: MessageType(enc.MessageType),
113114
}
114115
}
115116

errbase/encode.go

Lines changed: 101 additions & 13 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+
messageType := Prefix
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+
messageType = e.messageType
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, messageType = 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, messageType = 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+
Message: msg,
161+
Details: details,
162+
MessageType: errorspb.MessageType(messageType),
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, MessageType) {
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 "", Prefix
191+
}
171192
if strings.HasSuffix(prefix, ": ") {
172-
return prefix[:len(prefix)-2]
193+
return prefix[:len(prefix)-2], Prefix
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, FullMessage
176200
}
177201

178202
func getTypeDetails(
@@ -295,6 +319,35 @@ 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+
RegisterWrapperEncoderWithMessageType(
323+
theType,
324+
func(ctx context.Context, err error) (
325+
msgPrefix string,
326+
safeDetails []string,
327+
payload proto.Message,
328+
messageType MessageType,
329+
) {
330+
prefix, details, payload := encoder(ctx, err)
331+
return prefix, details, payload, messageType
332+
})
333+
}
334+
335+
// RegisterWrapperEncoderWithMessageType can be used to register
336+
// new wrapper types to the library. Registered wrappers will be
337+
// encoded using their own Go type when an error is encoded. Wrappers
338+
// that have not been registered will be encoded using the
339+
// opaqueWrapper type.
340+
//
341+
// This function differs from RegisterWrapperEncoder by allowing the
342+
// caller to explicitly decide whether the wrapper owns the entire
343+
// error message or not. Otherwise, the relationship is inferred.
344+
//
345+
// Note: if the error type has been migrated from a previous location
346+
// or a different type, ensure that RegisterTypeMigration() was called
347+
// prior to RegisterWrapperEncoder().
348+
func RegisterWrapperEncoderWithMessageType(
349+
theType TypeKey, encoder WrapperEncoderWithMessageType,
350+
) {
298351
if encoder == nil {
299352
delete(encoders, theType)
300353
} else {
@@ -304,7 +357,42 @@ func RegisterWrapperEncoder(theType TypeKey, encoder WrapperEncoder) {
304357

305358
// WrapperEncoder is to be provided (via RegisterWrapperEncoder above)
306359
// 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)
360+
type WrapperEncoder func(ctx context.Context, err error) (
361+
msgPrefix string,
362+
safeDetails []string,
363+
payload proto.Message,
364+
)
365+
366+
// MessageType is used to encode information about an error message
367+
// within a wrapper error type. This information is used to affect
368+
// display logic.
369+
type MessageType errorspb.MessageType
370+
371+
// Values below should match the ones in errorspb.MessageType for
372+
// direct conversion.
373+
const (
374+
// Prefix denotes an error message that should be prepended to the
375+
// message of its cause.
376+
Prefix MessageType = MessageType(errorspb.MessageType_PREFIX)
377+
// FullMessage denotes an error message that contains the text of its
378+
// causes and can be displayed standalone.
379+
FullMessage = MessageType(errorspb.MessageType_FULL_MESSAGE)
380+
)
381+
382+
// WrapperEncoderWithMessageType is to be provided (via
383+
// RegisterWrapperEncoderWithMessageType above) by additional wrapper
384+
// types not yet known to this library. This encoder returns an
385+
// additional enum which indicates whether the wrapper owns the error
386+
// message completely instead of simply being a prefix with the error
387+
// message of its causes appended to it. This information is encoded
388+
// along with the prefix in order to provide context during error
389+
// display.
390+
type WrapperEncoderWithMessageType func(ctx context.Context, err error) (
391+
msgPrefix string,
392+
safeDetails []string,
393+
payload proto.Message,
394+
messageType MessageType,
395+
)
308396

309397
// registry for RegisterWrapperType.
310-
var encoders = map[TypeKey]WrapperEncoder{}
398+
var encoders = map[TypeKey]WrapperEncoderWithMessageType{}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2023 The Cockroach Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
// implied. See the License for the specific language governing
13+
// permissions and limitations under the License.
14+
15+
package errbase_test
16+
17+
import (
18+
"context"
19+
goErr "errors"
20+
"fmt"
21+
"testing"
22+
23+
"github.com/cockroachdb/errors/errbase"
24+
"github.com/cockroachdb/errors/errorspb"
25+
"github.com/cockroachdb/errors/testutils"
26+
)
27+
28+
func genEncoded(mt errorspb.MessageType) errorspb.EncodedError {
29+
return errorspb.EncodedError{
30+
Error: &errorspb.EncodedError_Wrapper{
31+
Wrapper: &errorspb.EncodedWrapper{
32+
Cause: errorspb.EncodedError{
33+
Error: &errorspb.EncodedError_Leaf{
34+
Leaf: &errorspb.EncodedErrorLeaf{
35+
Message: "leaf-error-msg",
36+
},
37+
},
38+
},
39+
Message: "wrapper-error-msg: leaf-error-msg: extra info",
40+
Details: errorspb.EncodedErrorDetails{},
41+
MessageType: mt,
42+
},
43+
},
44+
}
45+
}
46+
47+
func TestDecodeOldVersion(t *testing.T) {
48+
tt := testutils.T{T: t}
49+
50+
errOldEncoded := genEncoded(errorspb.MessageType_PREFIX)
51+
errOldDecoded := errbase.DecodeError(context.Background(), errOldEncoded)
52+
// Ensure that we will continue to just concat leaf after wrapper
53+
// with older errors for backward compatibility.
54+
tt.CheckEqual(errOldDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info: leaf-error-msg")
55+
56+
// Check to ensure that when flag is present, we interpret things correctly.
57+
errNewEncoded := genEncoded(errorspb.MessageType_FULL_MESSAGE)
58+
errNewDecoded := errbase.DecodeError(context.Background(), errNewEncoded)
59+
tt.CheckEqual(errNewDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info")
60+
}
61+
62+
func TestEncodeDecodeNewVersion(t *testing.T) {
63+
tt := testutils.T{T: t}
64+
errNewEncoded := errbase.EncodeError(
65+
context.Background(),
66+
fmt.Errorf(
67+
"wrapper-error-msg: %w: extra info",
68+
goErr.New("leaf-error-msg"),
69+
),
70+
)
71+
72+
errNew := errorspb.EncodedError{
73+
Error: &errorspb.EncodedError_Wrapper{
74+
Wrapper: &errorspb.EncodedWrapper{
75+
Cause: errorspb.EncodedError{
76+
Error: &errorspb.EncodedError_Leaf{
77+
Leaf: &errorspb.EncodedErrorLeaf{
78+
Message: "leaf-error-msg",
79+
Details: errorspb.EncodedErrorDetails{
80+
OriginalTypeName: "errors/*errors.errorString",
81+
ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "errors/*errors.errorString", Extension: ""},
82+
ReportablePayload: nil,
83+
FullDetails: nil,
84+
},
85+
},
86+
},
87+
},
88+
Message: "wrapper-error-msg: leaf-error-msg: extra info",
89+
Details: errorspb.EncodedErrorDetails{
90+
OriginalTypeName: "fmt/*fmt.wrapError",
91+
ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "fmt/*fmt.wrapError", Extension: ""},
92+
ReportablePayload: nil,
93+
FullDetails: nil,
94+
},
95+
MessageType: errorspb.MessageType_FULL_MESSAGE,
96+
},
97+
},
98+
}
99+
100+
tt.CheckDeepEqual(errNewEncoded, errNew)
101+
newErr := errbase.DecodeError(context.Background(), errNew)
102+
103+
// New version correctly decodes error
104+
tt.CheckEqual(newErr.Error(), "wrapper-error-msg: leaf-error-msg: extra info")
105+
}

0 commit comments

Comments
 (0)