Skip to content

Commit 5197958

Browse files
authored
Merge pull request #113 from dhartunian/fix-wrapError-encode-decode
support wrapError encode and decode
2 parents 1456059 + 19be170 commit 5197958

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)