From 654231b2bda38b130d32a63724a2205389e6354f Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Fri, 18 Aug 2023 02:27:47 -0400 Subject: [PATCH] fix(gw): IPIP-402 CARs return useful blocks on not found errors (#440) Co-authored-by: Henrique Dias --- CHANGELOG.md | 2 ++ gateway/blocks_backend.go | 36 ++++++++++++++++++++++++++++++++++++ gateway/errors.go | 7 ++----- gateway/gateway_test.go | 2 +- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4489b13fd..c47ac1f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ The following emojis are used to highlight certain changes: ### Fixed - Address a Bitswap findpeers / connect race condition that can prevent peer communication ([#435](https://github.com/ipfs/boxo/issues/435)) +- HTTP Gateway API: Not having a block will result in a 5xx error rather than 404 +- HTTP Gateway API: CAR requests will return 200s and a CAR file proving a requested path does not exist rather than returning an error ### Security diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index 01ca49fec..82e7e1a3b 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -232,9 +232,45 @@ func (bb *BlocksBackend) Head(ctx context.Context, path ImmutablePath) (ContentP return md, fileNode, nil } +// emptyRoot is a CAR root with the empty identity CID. CAR files are recommended +// to always include a CID in their root, even if it's just the empty CID. +// https://ipld.io/specs/transport/car/carv1/#number-of-roots +var emptyRoot = []cid.Cid{cid.MustParse("bafkqaaa")} + func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { pathMetadata, err := bb.ResolvePath(ctx, p) if err != nil { + rootCid, err := cid.Decode(strings.Split(p.String(), "/")[2]) + if err != nil { + return ContentPathMetadata{}, nil, err + } + + var buf bytes.Buffer + cw, err := storage.NewWritable(&buf, emptyRoot, car.WriteAsCarV1(true)) + if err != nil { + return ContentPathMetadata{}, nil, err + } + + blockGetter := merkledag.NewDAGService(bb.blockService).Session(ctx) + + blockGetter = &nodeGetterToCarExporer{ + ng: blockGetter, + cw: cw, + } + + // Setup the UnixFS resolver. + f := newNodeGetterFetcherSingleUseFactory(ctx, blockGetter) + pathResolver := resolver.NewBasicResolver(f) + ip := ipfspath.FromString(p.String()) + _, _, err = pathResolver.ResolveToLastNode(ctx, ip) + + if isErrNotFound(err) { + return ContentPathMetadata{ + PathSegmentRoots: nil, + LastSegment: ifacepath.NewResolvedPath(ip, rootCid, rootCid, ""), + ContentType: "", + }, io.NopCloser(&buf), nil + } return ContentPathMetadata{}, nil, err } diff --git a/gateway/errors.go b/gateway/errors.go index e9671438e..17a22ca62 100644 --- a/gateway/errors.go +++ b/gateway/errors.go @@ -12,7 +12,6 @@ import ( "github.com/ipfs/boxo/gateway/assets" "github.com/ipfs/boxo/path/resolver" "github.com/ipfs/go-cid" - ipld "github.com/ipfs/go-ipld-format" "github.com/ipld/go-ipld-prime/datamodel" ) @@ -177,11 +176,9 @@ func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defa } } +// isErrNotFound returns true for IPLD errors that should return 4xx errors (e.g. the path doesn't exist, the data is +// the wrong type, etc.), rather than issues with just finding and retrieving the data. func isErrNotFound(err error) bool { - if ipld.IsNotFound(err) { - return true - } - // Checks if err is of a type that does not implement the .Is interface and // cannot be directly compared to. Therefore, errors.Is cannot be used. for { diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 7fae55f3e..1c988ac9c 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -763,7 +763,7 @@ func TestErrorBubblingFromBackend(t *testing.T) { }) } - testError("404 Not Found from IPLD", &ipld.ErrNotFound{}, http.StatusNotFound) + testError("500 Not Found from IPLD", &ipld.ErrNotFound{}, http.StatusInternalServerError) testError("404 Not Found from path resolver", resolver.ErrNoLink{}, http.StatusNotFound) testError("502 Bad Gateway", ErrBadGateway, http.StatusBadGateway) testError("504 Gateway Timeout", ErrGatewayTimeout, http.StatusGatewayTimeout)