Skip to content

Commit

Permalink
fix: types/errors Wrap and Wrapf (backport cosmos#10674) (cosmos#10676)
Browse files Browse the repository at this point in the history
* fix: types/errors Wrap and Wrapf (cosmos#10674)

<!--
The default pull request template is for types feat, fix, or refactor.
For other templates, add one of the following parameters to the url:
- template=docs.md
- template=other.md
-->

## Description

The `Error.Wrap`  and `Error.Wrapf` functions in `types/errors` didn't work with `Is` and `IsOf` because they had non-pointer receivers. This adds a fix and tests that failed and now pass.

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)

(cherry picked from commit 9566c99)

# Conflicts:
#	CHANGELOG.md

* fix conflict

Co-authored-by: Aaron Craelius <[email protected]>
Co-authored-by: marbar3778 <[email protected]>
  • Loading branch information
3 people authored and JeancarloBarrios committed Sep 28, 2024
1 parent fbb7e23 commit f88e688
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 5 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [\#10414](https://github.com/cosmos/cosmos-sdk/pull/10414) Use `sdk.GetConfig().GetFullBIP44Path()` instead `sdk.FullFundraiserPath` to generate key
* (bank) [\#10394](https://github.com/cosmos/cosmos-sdk/pull/10394) Fix: query account balance by ibc denom.
* [\10608](https://github.com/cosmos/cosmos-sdk/pull/10608) Change the order of module migration by pushing x/auth to the end. Auth module depends on other modules and should be run last. We have updated the documentation to provide more details how to change module migration order. This is technically a breaking change, but only impacts updates between the upgrades with version change, hence migrating from the previous patch release doesn't cause new migration and doesn't break the state.

* [\#10674](https://github.com/cosmos/cosmos-sdk/pull/10674) Fix issue with `Error.Wrap` and `Error.Wrapf` usage with `errors.Is`.
## [v0.44.3](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.44.3) - 2021-10-21

### Improvements
Expand Down
249 changes: 245 additions & 4 deletions types/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,248 @@ var (
// ErrPanic should only be set when we recovering from a panic
ErrPanic = errorsmod.ErrPanic

// ErrTxTimeout defines an error for when a tx is rejected out due to an
// explicitly set timeout timestamp.
ErrTxTimeout = errorsmod.Register(RootCodespace, 42, "tx timeout")
)
// Register returns an error instance that should be used as the base for
// creating error instances during runtime.
//
// Popular root errors are declared in this package, but extensions may want to
// declare custom codes. This function ensures that no error code is used
// twice. Attempt to reuse an error code results in panic.
//
// Use this function only during a program startup phase.
func Register(codespace string, code uint32, description string) *Error {
// TODO - uniqueness is (codespace, code) combo
if e := getUsed(codespace, code); e != nil {
panic(fmt.Sprintf("error with code %d is already registered: %q", code, e.desc))
}

err := New(codespace, code, description)
setUsed(err)

return err
}

// usedCodes is keeping track of used codes to ensure their uniqueness. No two
// error instances should share the same (codespace, code) tuple.
var usedCodes = map[string]*Error{}

func errorID(codespace string, code uint32) string {
return fmt.Sprintf("%s:%d", codespace, code)
}

func getUsed(codespace string, code uint32) *Error {
return usedCodes[errorID(codespace, code)]
}

func setUsed(err *Error) {
usedCodes[errorID(err.codespace, err.code)] = err
}

// ABCIError will resolve an error code/log from an abci result into
// an error message. If the code is registered, it will map it back to
// the canonical error, so we can do eg. ErrNotFound.Is(err) on something
// we get back from an external API.
//
// This should *only* be used in clients, not in the server side.
// The server (abci app / blockchain) should only refer to registered errors
func ABCIError(codespace string, code uint32, log string) error {
if e := getUsed(codespace, code); e != nil {
return Wrap(e, log)
}
// This is a unique error, will never match on .Is()
// Use Wrap here to get a stack trace
return Wrap(New(codespace, code, "unknown"), log)
}

// Error represents a root error.
//
// Weave framework is using root error to categorize issues. Each instance
// created during the runtime should wrap one of the declared root errors. This
// allows error tests and returning all errors to the client in a safe manner.
//
// All popular root errors are declared in this package. If an extension has to
// declare a custom root error, always use Register function to ensure
// error code uniqueness.
type Error struct {
codespace string
code uint32
desc string
}

func New(codespace string, code uint32, desc string) *Error {
return &Error{codespace: codespace, code: code, desc: desc}
}

func (e Error) Error() string {
return e.desc
}

func (e Error) ABCICode() uint32 {
return e.code
}

func (e Error) Codespace() string {
return e.codespace
}

// Is check if given error instance is of a given kind/type. This involves
// unwrapping given error using the Cause method if available.
func (e *Error) Is(err error) bool {
// Reflect usage is necessary to correctly compare with
// a nil implementation of an error.
if e == nil {
return isNilErr(err)
}

for {
if err == e {
return true
}

// If this is a collection of errors, this function must return
// true if at least one from the group match.
if u, ok := err.(unpacker); ok {
for _, er := range u.Unpack() {
if e.Is(er) {
return true
}
}
}

if c, ok := err.(causer); ok {
err = c.Cause()
} else {
return false
}
}
}

// Wrap extends this error with an additional information.
// It's a handy function to call Wrap with sdk errors.
func (e *Error) Wrap(desc string) error { return Wrap(e, desc) }

// Wrapf extends this error with an additional information.
// It's a handy function to call Wrapf with sdk errors.
func (e *Error) Wrapf(desc string, args ...interface{}) error { return Wrapf(e, desc, args...) }

func isNilErr(err error) bool {
// Reflect usage is necessary to correctly compare with
// a nil implementation of an error.
if err == nil {
return true
}
if reflect.ValueOf(err).Kind() == reflect.Struct {
return false
}
return reflect.ValueOf(err).IsNil()
}

// Wrap extends given error with an additional information.
//
// If the wrapped error does not provide ABCICode method (ie. stdlib errors),
// it will be labeled as internal error.
//
// If err is nil, this returns nil, avoiding the need for an if statement when
// wrapping a error returned at the end of a function
func Wrap(err error, description string) error {
if err == nil {
return nil
}

// If this error does not carry the stacktrace information yet, attach
// one. This should be done only once per error at the lowest frame
// possible (most inner wrap).
if stackTrace(err) == nil {
err = errors.WithStack(err)
}

return &wrappedError{
parent: err,
msg: description,
}
}

// Wrapf extends given error with an additional information.
//
// This function works like Wrap function with additional functionality of
// formatting the input as specified.
func Wrapf(err error, format string, args ...interface{}) error {
desc := fmt.Sprintf(format, args...)
return Wrap(err, desc)
}

type wrappedError struct {
// This error layer description.
msg string
// The underlying error that triggered this one.
parent error
}

func (e *wrappedError) Error() string {
return fmt.Sprintf("%s: %s", e.msg, e.parent.Error())
}

func (e *wrappedError) Cause() error {
return e.parent
}

// Is reports whether any error in e's chain matches a target.
func (e *wrappedError) Is(target error) bool {
if e == target {
return true
}

w := e.Cause()
for {
if w == target {
return true
}

x, ok := w.(causer)
if ok {
w = x.Cause()
}
if x == nil {
return false
}
}
}

// Unwrap implements the built-in errors.Unwrap
func (e *wrappedError) Unwrap() error {
return e.parent
}

// Recover captures a panic and stop its propagation. If panic happens it is
// transformed into a ErrPanic instance and assigned to given error. Call this
// function using defer in order to work as expected.
func Recover(err *error) {
if r := recover(); r != nil {
*err = Wrapf(ErrPanic, "%v", r)
}
}

// WithType is a helper to augment an error with a corresponding type message
func WithType(err error, obj interface{}) error {
return Wrap(err, fmt.Sprintf("%T", obj))
}

// IsOf checks if a received error is caused by one of the target errors.
// It extends the errors.Is functionality to a list of errors.
func IsOf(received error, targets ...error) bool {
for _, t := range targets {
if errors.Is(received, t) {
return true
}
}
return false
}

// causer is an interface implemented by an error that supports wrapping. Use
// it to test if an error wraps another error instance.
type causer interface {
Cause() error
}

type unpacker interface {
Unpack() []error
}

0 comments on commit f88e688

Please sign in to comment.