diff --git a/client/httpapi/block.go b/client/httpapi/block.go index 640f186f5..c074f7940 100644 --- a/client/httpapi/block.go +++ b/client/httpapi/block.go @@ -3,7 +3,6 @@ package httpapi import ( "bytes" "context" - "errors" "fmt" "io" @@ -67,7 +66,7 @@ func (api *BlockAPI) Get(ctx context.Context, p path.Path) (io.Reader, error) { return nil, err } if resp.Error != nil { - return nil, resp.Error + return nil, parseErrNotFoundWithFallbackToError(resp.Error) } //TODO: make get return ReadCloser to avoid copying @@ -99,18 +98,14 @@ func (api *BlockAPI) Rm(ctx context.Context, p path.Path, opts ...caopts.BlockRm return err } - if removedBlock.Error != "" { - return errors.New(removedBlock.Error) - } - - return nil + return parseErrNotFoundWithFallbackToMSG(removedBlock.Error) } func (api *BlockAPI) Stat(ctx context.Context, p path.Path) (iface.BlockStat, error) { var out blockStat err := api.core().Request("block/stat", p.String()).Exec(ctx, &out) if err != nil { - return nil, err + return nil, parseErrNotFoundWithFallbackToError(err) } out.cid, err = cid.Parse(out.Key) if err != nil { diff --git a/client/httpapi/errors.go b/client/httpapi/errors.go new file mode 100644 index 000000000..1ccf6182c --- /dev/null +++ b/client/httpapi/errors.go @@ -0,0 +1,169 @@ +package httpapi + +import ( + "errors" + "strings" + "unicode/utf8" + + "github.com/ipfs/go-cid" + ipld "github.com/ipfs/go-ipld-format" + mbase "github.com/multiformats/go-multibase" +) + +// This file handle parsing and returning the correct ABI based errors from error messages + +type prePostWrappedNotFoundError struct { + pre string + post string + + wrapped ipld.ErrNotFound +} + +func (e prePostWrappedNotFoundError) String() string { + return e.Error() +} + +func (e prePostWrappedNotFoundError) Error() string { + return e.pre + e.wrapped.Error() + e.post +} + +func (e prePostWrappedNotFoundError) Unwrap() error { + return e.wrapped +} + +func parseErrNotFoundWithFallbackToMSG(msg string) error { + err, handled := parseErrNotFound(msg) + if handled { + return err + } + + return errors.New(msg) +} + +func parseErrNotFoundWithFallbackToError(msg error) error { + err, handled := parseErrNotFound(msg.Error()) + if handled { + return err + } + + return msg +} + +//lint:ignore ST1008 this function is not using the error as a mean to return failure but it massages it to return the correct type +func parseErrNotFound(msg string) (error, bool) { + if msg == "" { + return nil, true // Fast path + } + + if err, handled := parseIPLDErrNotFound(msg); handled { + return err, true + } + + if err, handled := parseBlockstoreNotFound(msg); handled { + return err, true + } + + return nil, false +} + +// Assume CIDs break on: +// - Whitespaces: " \t\n\r\v\f" +// - Semicolon: ";" this is to parse ipld.ErrNotFound wrapped in multierr +// - Double Quotes: "\"" this is for parsing %q and %#v formating +const cidBreakSet = " \t\n\r\v\f;\"" + +//lint:ignore ST1008 using error as values +func parseIPLDErrNotFound(msg string) (error, bool) { + // The patern we search for is: + const ipldErrNotFoundKey = "ipld: could not find " /*CID*/ + // We try to parse the CID, if it's invalid we give up and return a simple text error. + // We also accept "node" in place of the CID because that means it's an Undefined CID. + + keyIndex := strings.Index(msg, ipldErrNotFoundKey) + + if keyIndex < 0 { // Unknown error + return nil, false + } + + cidStart := keyIndex + len(ipldErrNotFoundKey) + + msgPostKey := msg[cidStart:] + var c cid.Cid + var postIndex int + if strings.HasPrefix(msgPostKey, "node") { + // Fallback case + c = cid.Undef + postIndex = len("node") + } else { + postIndex = strings.IndexFunc(msgPostKey, func(r rune) bool { + return strings.ContainsAny(string(r), cidBreakSet) + }) + if postIndex < 0 { + // no breakage meaning the string look like this something + "ipld: could not find bafy" + postIndex = len(msgPostKey) + } + + cidStr := msgPostKey[:postIndex] + + var err error + c, err = cid.Decode(cidStr) + if err != nil { + // failed to decode CID give up + return nil, false + } + + // check that the CID is either a CIDv0 or a base32 multibase + // because that what ipld.ErrNotFound.Error() -> cid.Cid.String() do currently + if c.Version() != 0 { + baseRune, _ := utf8.DecodeRuneInString(cidStr) + if baseRune == utf8.RuneError || baseRune != mbase.Base32 { + // not a multibase we expect, give up + return nil, false + } + } + } + + err := ipld.ErrNotFound{Cid: c} + pre := msg[:keyIndex] + post := msgPostKey[postIndex:] + + if len(pre) > 0 || len(post) > 0 { + return prePostWrappedNotFoundError{ + pre: pre, + post: post, + wrapped: err, + }, true + } + + return err, true +} + +// This is a simple error type that just return msg as Error(). +// But that also match ipld.ErrNotFound when called with Is(err). +// That is needed to keep compatiblity with code that use string.Contains(err.Error(), "blockstore: block not found") +// and code using ipld.ErrNotFound +type blockstoreNotFoundMatchingIPLDErrNotFound struct { + msg string +} + +func (e blockstoreNotFoundMatchingIPLDErrNotFound) String() string { + return e.Error() +} + +func (e blockstoreNotFoundMatchingIPLDErrNotFound) Error() string { + return e.msg +} + +func (e blockstoreNotFoundMatchingIPLDErrNotFound) Is(err error) bool { + _, ok := err.(ipld.ErrNotFound) + return ok +} + +//lint:ignore ST1008 using error as values +func parseBlockstoreNotFound(msg string) (error, bool) { + if !strings.Contains(msg, "blockstore: block not found") { + return nil, false + } + + return blockstoreNotFoundMatchingIPLDErrNotFound{msg: msg}, true +} diff --git a/client/httpapi/errors_test.go b/client/httpapi/errors_test.go new file mode 100644 index 000000000..c8b98d08e --- /dev/null +++ b/client/httpapi/errors_test.go @@ -0,0 +1,95 @@ +package httpapi + +import ( + "errors" + "fmt" + "testing" + + "github.com/ipfs/go-cid" + ipld "github.com/ipfs/go-ipld-format" + mbase "github.com/multiformats/go-multibase" + mh "github.com/multiformats/go-multihash" +) + +var randomSha256MH = mh.Multihash{0x12, 0x20, 0x88, 0x82, 0x73, 0x37, 0x7c, 0xc1, 0xc9, 0x96, 0xad, 0xee, 0xd, 0x26, 0x84, 0x2, 0xc9, 0xc9, 0x5c, 0xf9, 0x5c, 0x4d, 0x9b, 0xc3, 0x3f, 0xfb, 0x4a, 0xd8, 0xaf, 0x28, 0x6b, 0xca, 0x1a, 0xf2} + +func doParseIpldNotFoundTest(t *testing.T, original error) { + originalMsg := original.Error() + + rebuilt := parseErrNotFoundWithFallbackToMSG(originalMsg) + + rebuiltMsg := rebuilt.Error() + + if originalMsg != rebuiltMsg { + t.Errorf("expected message to be %q; got %q", originalMsg, rebuiltMsg) + } + + originalNotFound := ipld.IsNotFound(original) + rebuiltNotFound := ipld.IsNotFound(rebuilt) + if originalNotFound != rebuiltNotFound { + t.Errorf("for %q expected Ipld.IsNotFound to be %t; got %t", originalMsg, originalNotFound, rebuiltNotFound) + } +} + +func TestParseIPLDNotFound(t *testing.T) { + if err := parseErrNotFoundWithFallbackToMSG(""); err != nil { + t.Errorf("expected empty string to give no error; got %T %q", err, err.Error()) + } + + cidBreaks := make([]string, len(cidBreakSet)) + for i, v := range cidBreakSet { + cidBreaks[i] = "%w" + string(v) + } + + base58BTCEncoder, err := mbase.NewEncoder(mbase.Base58BTC) + if err != nil { + t.Fatalf("expected to find Base58BTC encoder; got error %q", err.Error()) + } + + for _, wrap := range append(cidBreaks, + "", + "merkledag: %w", + "testing: %w the test", + "%w is wrong", + ) { + for _, err := range [...]error{ + errors.New("ipld: could not find "), + errors.New("ipld: could not find Bad_CID"), + errors.New("ipld: could not find " + cid.NewCidV1(cid.Raw, randomSha256MH).Encode(base58BTCEncoder)), // Test that we only accept CIDv0 and base32 CIDs + errors.New("network connection timeout"), + ipld.ErrNotFound{Cid: cid.Undef}, + ipld.ErrNotFound{Cid: cid.NewCidV0(randomSha256MH)}, + ipld.ErrNotFound{Cid: cid.NewCidV1(cid.Raw, randomSha256MH)}, + } { + if wrap != "" { + err = fmt.Errorf(wrap, err) + } + + doParseIpldNotFoundTest(t, err) + } + } +} + +func TestBlockstoreNotFoundMatchingIPLDErrNotFound(t *testing.T) { + if !ipld.IsNotFound(blockstoreNotFoundMatchingIPLDErrNotFound{}) { + t.Fatalf("expected blockstoreNotFoundMatchingIPLDErrNotFound to match ipld.IsNotFound; got false") + } + + for _, wrap := range [...]string{ + "", + "merkledag: %w", + "testing: %w the test", + "%w is wrong", + } { + for _, err := range [...]error{ + errors.New("network connection timeout"), + blockstoreNotFoundMatchingIPLDErrNotFound{"blockstore: block not found"}, + } { + if wrap != "" { + err = fmt.Errorf(wrap, err) + } + + doParseIpldNotFoundTest(t, err) + } + } +}