Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The following emojis are used to highlight certain changes:
- Added retrieval state tracking for timeout diagnostics. When retrieval timeouts occur, the error messages now include detailed information about which phase failed (path resolution, provider discovery, connecting, or data retrieval) and provider statistics including failed peer IDs [#1015](https://github.com/ipfs/boxo/pull/1015) [#1023](https://github.com/ipfs/boxo/pull/1023)
- Added `Config.DiagnosticServiceURL` to configure a CID retrievability diagnostic service. When set, 504 Gateway Timeout errors show a "Check CID retrievability" button linking to the service with `?cid=<failed-cid>` [#1023](https://github.com/ipfs/boxo/pull/1023)
- Improved 504 error pages with "Retry" button, diagnostic service integration, and clear indication when timeout occurs on sub-resource vs root CID [#1023](https://github.com/ipfs/boxo/pull/1023)
- `gateway`: Added `Config.MaxRangeRequestFileSize` to protect against CDN issues with large file range requests. When set to a non-zero value, range requests for files larger than this limit return HTTP 501 Not Implemented with a suggestion to use verifiable block requests (`application/vnd.ipld.raw`) instead. This provides protection against Cloudflare's issue where range requests for files over 5GiB are silently ignored, causing excess bandwidth consumption and billing

### Changed

Expand Down
7 changes: 7 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ type Config struct {
// button is not shown.
DiagnosticServiceURL string

// MaxRangeRequestFileSize is the maximum file size in bytes for which range
// requests are supported. When set to a value greater than 0, range requests
// for files larger than this limit will return 501 Not Implemented error.
// This provides protection against CDN/proxy issues with large files
// (e.g., Cloudflare's 5GB limit). A value of 0 disables this limit.
MaxRangeRequestFileSize int64

// MetricsRegistry is the Prometheus registry to use for metrics.
// If nil, prometheus.DefaultRegisterer will be used.
MetricsRegistry prometheus.Registerer
Expand Down
98 changes: 98 additions & 0 deletions gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1377,3 +1377,101 @@ func TestBrowserErrorHTML(t *testing.T) {
require.Contains(t, string(body), "<!DOCTYPE html>")
})
}

func TestMaxRangeRequestFileSize(t *testing.T) {
backend, root := newMockBackend(t, "fixtures.car")

// Get a file path from the fixtures
p, err := path.Join(path.FromCid(root), "subdir", "fnord")
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

k, err := backend.resolvePathNoRootsReturned(ctx, p)
require.NoError(t, err)

t.Run("Range request exceeds file size limit returns 501", func(t *testing.T) {
// Create a test server with very small limit (smaller than "fnord" file which is 5 bytes)
ts := newTestServerWithConfig(t, backend, Config{
DeserializedResponses: true,
MaxRangeRequestFileSize: 4, // 4 bytes limit - smaller than "fnord" (5 bytes)
})
defer ts.Close()

// Range request should fail with 501
req, err := http.NewRequest(http.MethodGet, ts.URL+k.String(), nil)
require.NoError(t, err)
req.Header.Set("Range", "bytes=0-4")

res := mustDoWithoutRedirect(t, req)
require.Equal(t, http.StatusNotImplemented, res.StatusCode)

body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Contains(t, string(body), "range requests not supported for files larger than 4 bytes")
require.Contains(t, string(body), "switch to verifiable block requests (application/vnd.ipld.raw)")
})

t.Run("Range request within file size limit works", func(t *testing.T) {
// Create a test server with limit larger than the file
ts := newTestServerWithConfig(t, backend, Config{
DeserializedResponses: true,
MaxRangeRequestFileSize: 1000, // 1KB limit - larger than "fnord" (5 bytes)
})
defer ts.Close()

// Range request should work
req, err := http.NewRequest(http.MethodGet, ts.URL+k.String(), nil)
require.NoError(t, err)
req.Header.Set("Range", "bytes=0-4")

res := mustDoWithoutRedirect(t, req)
require.Equal(t, http.StatusPartialContent, res.StatusCode)

body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "fnord", string(body))
})

t.Run("Regular request works regardless of file size limit", func(t *testing.T) {
// Create a test server with very small limit
ts := newTestServerWithConfig(t, backend, Config{
DeserializedResponses: true,
MaxRangeRequestFileSize: 1, // 1 byte limit - much smaller than any file
})
defer ts.Close()

// Regular request without Range header should work regardless of limit
req, err := http.NewRequest(http.MethodGet, ts.URL+k.String(), nil)
require.NoError(t, err)

res := mustDoWithoutRedirect(t, req)
require.Equal(t, http.StatusOK, res.StatusCode)

body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "fnord", string(body))
})

t.Run("MaxRangeRequestFileSize disabled when set to 0", func(t *testing.T) {
// Create test server with limit disabled
ts := newTestServerWithConfig(t, backend, Config{
DeserializedResponses: true,
MaxRangeRequestFileSize: 0, // Disabled
})
defer ts.Close()

// Range request should work regardless of file size
req, err := http.NewRequest(http.MethodGet, ts.URL+k.String(), nil)
require.NoError(t, err)
req.Header.Set("Range", "bytes=0-4")

res := mustDoWithoutRedirect(t, req)
require.Equal(t, http.StatusPartialContent, res.StatusCode)

body, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "fnord", string(body))
})
}
8 changes: 8 additions & 0 deletions gateway/handler_unixfs_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gateway
import (
"bytes"
"context"
"fmt"
"io"
"mime"
"net/http"
Expand All @@ -22,6 +23,13 @@ func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.
_, span := spanTrace(ctx, "Handler.ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
defer span.End()

// Check if range request exceeds file size limit
if i.config.MaxRangeRequestFileSize > 0 && r.Header.Get("Range") != "" && fileSize > i.config.MaxRangeRequestFileSize {
err := fmt.Errorf("range requests not supported for files larger than %d bytes: switch to verifiable block requests (application/vnd.ipld.raw)", i.config.MaxRangeRequestFileSize)
i.webError(w, r, err, http.StatusNotImplemented)
return false
}

// Set Cache-Control and read optional Last-Modified time
modtime := addCacheControlHeaders(w, r, rq.contentPath, rq.ttl, rq.lastMod, resolvedPath.RootCid(), "")

Expand Down
Loading