Skip to content

Commit

Permalink
gateway: import backend remote utils
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Mar 7, 2024
1 parent ad56504 commit a994a0a
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 0 deletions.
1 change: 1 addition & 0 deletions gateway/backend_remote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package gateway
74 changes: 74 additions & 0 deletions gateway/backend_remote_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package gateway

import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"

"github.com/ipfs/boxo/path"
)

// contentPathToCarUrl returns an URL that allows retrieval of specified resource
// from a trustless gateway that implements IPIP-402
func contentPathToCarUrl(path path.ImmutablePath, params CarParams) *url.URL {
return &url.URL{
Path: path.String(),
RawQuery: carParamsToString(params),
}
}

// carParamsToString converts CarParams to URL parameters compatible with IPIP-402
func carParamsToString(params CarParams) string {
paramsBuilder := strings.Builder{}
paramsBuilder.WriteString("format=car") // always send explicit format in URL, this makes debugging easier, even when Accept header was set
if params.Scope != "" {
paramsBuilder.WriteString("&dag-scope=")
paramsBuilder.WriteString(string(params.Scope))
}
if params.Range != nil {
paramsBuilder.WriteString("&entity-bytes=")
paramsBuilder.WriteString(strconv.FormatInt(params.Range.From, 10))
paramsBuilder.WriteString(":")
if params.Range.To != nil {
paramsBuilder.WriteString(strconv.FormatInt(*params.Range.To, 10))
} else {
paramsBuilder.WriteString("*")
}
}
return paramsBuilder.String()
}

// TODO: do not export this?
// 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, &ErrorStatusCode{}) ||
errors.Is(err, &ErrorRetryAfter{}) {
// already correct error
return err
}

// All timeouts should produce 504 Gateway Timeout
if errors.Is(err, context.DeadlineExceeded) || // TODO: already handled in [webError], right?
// errors.Is(err, caboose.ErrTimeout) || // TODO: We're removing this
// 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", ErrGatewayTimeout, err.Error())
}

// TODO: return this?
// (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 NewErrorRetryAfter(err, r.RetryAfter())
}
}

// everything else returns 502 Bad Gateway
return fmt.Errorf("%w: %s", ErrBadGateway, err.Error())
}
98 changes: 98 additions & 0 deletions gateway/backend_remote_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package gateway

import (
"errors"
"fmt"
"testing"
"time"

"github.com/ipfs/boxo/path"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestContentPathToCarUrl(t *testing.T) {
t.Parallel()

negativeOffset := int64(-42)
testCases := []struct {
contentPath string // to be turned into ImmutablePath
carParams CarParams
expectedUrl string // url.URL.String()
}{
{
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
carParams: CarParams{},
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car",
},
{
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 0, To: nil}},
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=entity&entity-bytes=0:*",
},
{
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
carParams: CarParams{Scope: "block"},
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=block",
},
{
contentPath: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 4, To: &negativeOffset}},
expectedUrl: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi?format=car&dag-scope=entity&entity-bytes=4:-42",
},
{
// a regression test for case described in https://github.com/ipfs/gateway-conformance/issues/115
contentPath: "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze/I/Auditorio_de_Tenerife%2C_Santa_Cruz_de_Tenerife%2C_España%2C_2012-12-15%2C_DD_02.jpg.webp",
carParams: CarParams{Scope: "entity", Range: &DagByteRange{From: 0, To: nil}},
expectedUrl: "/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze/I/Auditorio_de_Tenerife%252C_Santa_Cruz_de_Tenerife%252C_Espa%C3%B1a%252C_2012-12-15%252C_DD_02.jpg.webp?format=car&dag-scope=entity&entity-bytes=0:*",
},
}

for _, tc := range testCases {
t.Run("TestContentPathToCarUrl", func(t *testing.T) {
p, err := path.NewPath(tc.contentPath)
require.NoError(t, err)

contentPath, err := path.NewImmutablePath(p)
require.NoError(t, err)

result := contentPathToCarUrl(contentPath, tc.carParams).String()
require.Equal(t, tc.expectedUrl, result)
})
}
}

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) {
t.Parallel()

originalErr := &testErr{message: "test", retryAfter: time.Minute}
var (
convertedErr error
gatewayErr *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)
}

0 comments on commit a994a0a

Please sign in to comment.