diff --git a/blockstore.go b/blockstore.go index 571b436..825cb08 100644 --- a/blockstore.go +++ b/blockstore.go @@ -3,14 +3,11 @@ package main import ( "context" "errors" - "fmt" - "strings" "time" - "github.com/filecoin-saturn/caboose" + "github.com/ipfs/bifrost-gateway/lib" blockstore "github.com/ipfs/boxo/blockstore" exchange "github.com/ipfs/boxo/exchange" - "github.com/ipfs/boxo/gateway" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "go.uber.org/zap/zapcore" @@ -38,7 +35,7 @@ func (e *exchangeBsWrapper) GetBlock(ctx context.Context, c cid.Cid) (blocks.Blo blk, err := e.bstore.Get(ctx, c) if err != nil { - return nil, gatewayError(err) + return nil, lib.GatewayError(err) } return blk, nil } @@ -67,33 +64,4 @@ func (e *exchangeBsWrapper) Close() error { return nil } -// gatewayError translates underlying blockstore error into one that gateway code will return as HTTP 502 or 504 -// it also makes sure Retry-After hint from remote blockstore will be passed to HTTP client, if present. -func gatewayError(err error) error { - if errors.Is(err, &gateway.ErrorStatusCode{}) || - errors.Is(err, &gateway.ErrorRetryAfter{}) { - // already correct error - return err - } - - // All timeouts should produce 504 Gateway Timeout - if errors.Is(err, context.DeadlineExceeded) || - errors.Is(err, caboose.ErrSaturnTimeout) || - // Unfortunately this is not an exported type so we have to check for the content. - strings.Contains(err.Error(), "Client.Timeout exceeded") { - return fmt.Errorf("%w: %s", gateway.ErrGatewayTimeout, err.Error()) - } - - // (Saturn) errors that support the RetryAfter interface need to be converted - // to the correct gateway error, such that the HTTP header is set. - for v := err; v != nil; v = errors.Unwrap(v) { - if r, ok := v.(interface{ RetryAfter() time.Duration }); ok { - return gateway.NewErrorRetryAfter(err, r.RetryAfter()) - } - } - - // everything else returns 502 Bad Gateway - return fmt.Errorf("%w: %s", gateway.ErrBadGateway, err.Error()) -} - var _ exchange.Interface = (*exchangeBsWrapper)(nil) diff --git a/blockstore_test.go b/blockstore_test.go deleted file mode 100644 index c47827a..0000000 --- a/blockstore_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "testing" - "time" - - "github.com/ipfs/boxo/gateway" - "github.com/stretchr/testify/assert" -) - -type testErr struct { - message string - retryAfter time.Duration -} - -func (e *testErr) Error() string { - return e.message -} - -func (e *testErr) RetryAfter() time.Duration { - return e.retryAfter -} - -func TestGatewayErrorRetryAfter(t *testing.T) { - originalErr := &testErr{message: "test", retryAfter: time.Minute} - var ( - convertedErr error - gatewayErr *gateway.ErrorRetryAfter - ) - - // Test unwrapped - convertedErr = gatewayError(originalErr) - ok := errors.As(convertedErr, &gatewayErr) - assert.True(t, ok) - assert.EqualValues(t, originalErr.retryAfter, gatewayErr.RetryAfter) - - // Test wrapped. - convertedErr = gatewayError(fmt.Errorf("wrapped error: %w", originalErr)) - ok = errors.As(convertedErr, &gatewayErr) - assert.True(t, ok) - assert.EqualValues(t, originalErr.retryAfter, gatewayErr.RetryAfter) -} diff --git a/lib/gateway_traversal.go b/lib/gateway_traversal.go index 461efde..2acdac2 100644 --- a/lib/gateway_traversal.go +++ b/lib/gateway_traversal.go @@ -89,7 +89,7 @@ func carToLinearBlockGetter(ctx context.Context, reader io.Reader, metrics *Grap if !ok || errors.Is(blkRead.err, io.EOF) { return nil, io.ErrUnexpectedEOF } - return nil, blkRead.err + return nil, GatewayError(blkRead.err) } if blkRead.block != nil { metrics.carBlocksFetchedMetric.Inc() diff --git a/lib/gateway_utils.go b/lib/gateway_utils.go index bee79a5..e7093fb 100644 --- a/lib/gateway_utils.go +++ b/lib/gateway_utils.go @@ -1,10 +1,15 @@ package lib import ( + "context" + "errors" + "fmt" "net/url" "strconv" "strings" + "time" + "github.com/filecoin-saturn/caboose" "github.com/ipfs/boxo/gateway" ) @@ -37,3 +42,32 @@ func carParamsToString(params gateway.CarParams) string { } return paramsBuilder.String() } + +// GatewayError translates underlying blockstore error into one that gateway code will return as HTTP 502 or 504 +// it also makes sure Retry-After hint from remote blockstore will be passed to HTTP client, if present. +func GatewayError(err error) error { + if errors.Is(err, &gateway.ErrorStatusCode{}) || + errors.Is(err, &gateway.ErrorRetryAfter{}) { + // already correct error + return err + } + + // All timeouts should produce 504 Gateway Timeout + if errors.Is(err, context.DeadlineExceeded) || + errors.Is(err, caboose.ErrSaturnTimeout) || + // Unfortunately this is not an exported type so we have to check for the content. + strings.Contains(err.Error(), "Client.Timeout exceeded") { + return fmt.Errorf("%w: %s", gateway.ErrGatewayTimeout, err.Error()) + } + + // (Saturn) errors that support the RetryAfter interface need to be converted + // to the correct gateway error, such that the HTTP header is set. + for v := err; v != nil; v = errors.Unwrap(v) { + if r, ok := v.(interface{ RetryAfter() time.Duration }); ok { + return gateway.NewErrorRetryAfter(err, r.RetryAfter()) + } + } + + // everything else returns 502 Bad Gateway + return fmt.Errorf("%w: %s", gateway.ErrBadGateway, err.Error()) +} diff --git a/lib/gateway_utils_test.go b/lib/gateway_utils_test.go index dc7db0b..82fb655 100644 --- a/lib/gateway_utils_test.go +++ b/lib/gateway_utils_test.go @@ -1,7 +1,12 @@ package lib import ( + "errors" + "fmt" "testing" + "time" + + "github.com/stretchr/testify/assert" ifacepath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/gateway" @@ -55,3 +60,36 @@ func TestContentPathToCarUrl(t *testing.T) { }) } } + +type testErr struct { + message string + retryAfter time.Duration +} + +func (e *testErr) Error() string { + return e.message +} + +func (e *testErr) RetryAfter() time.Duration { + return e.retryAfter +} + +func TestGatewayErrorRetryAfter(t *testing.T) { + originalErr := &testErr{message: "test", retryAfter: time.Minute} + var ( + convertedErr error + gatewayErr *gateway.ErrorRetryAfter + ) + + // Test unwrapped + convertedErr = GatewayError(originalErr) + ok := errors.As(convertedErr, &gatewayErr) + assert.True(t, ok) + assert.EqualValues(t, originalErr.retryAfter, gatewayErr.RetryAfter) + + // Test wrapped. + convertedErr = GatewayError(fmt.Errorf("wrapped error: %w", originalErr)) + ok = errors.As(convertedErr, &gatewayErr) + assert.True(t, ok) + assert.EqualValues(t, originalErr.retryAfter, gatewayErr.RetryAfter) +}