Skip to content

Commit bb4780c

Browse files
authored
test: range requests of deserialized files (#213)
* test: suffix range request * chore: adjust range to not start at 0 makes test more representative of seeking inside of a partially available file * chore: focus on Content-Range content-type is not hard requirement, and implementations may have easier or harder time with coming up with the right one. if the content type info is not part of DAG metadata, and needs to me sniffed (majority of content in 2025), then this internal sniffing may produce different results in different file x library permutations. in practice, it does not matter, browsers will stream videos just fine. lets focus on Content-Range and body being correct - if we need to test Content-Type, let's move it to a different test suite * chore: human-readable check results * test: Accept-Ranges: bytes present ensure behavior described in ipfs/boxo#856 (comment) is covered by test * chore: release as 0.8.0
1 parent b5df2c0 commit bb4780c

File tree

4 files changed

+113
-48
lines changed

4 files changed

+113
-48
lines changed

.github/workflows/test-kubo-e2e.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
defaults:
1818
run:
1919
shell: bash
20+
permissions:
21+
pull-requests: write # Required for commenting on pull requests
2022
steps:
2123
- name: Setup Go
2224
uses: actions/setup-go@v4
@@ -80,13 +82,13 @@ jobs:
8082
- name: Find latest comment
8183
id: find-comment
8284
if: github.event.pull_request
83-
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
85+
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
8486
with:
8587
issue-number: ${{ github.event.pull_request.number }}
8688
body-includes: "Results against Kubo ${{ matrix.target }}"
8789
- name: Create comment
8890
if: github.event.pull_request
89-
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
91+
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
9092
with:
9193
issue-number: ${{ github.event.pull_request.number }}
9294
comment-id: ${{ steps.find-comment.outputs.comment-id }}

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.8.0] - 2025-05-28
8+
### Changed
9+
- Comprehensive tests for HTTP Range Requests over deserialized UnixFS files have been added. The `--specs path-gateway` now requires support for at least single-range requests. Deserialized range-requests can be skipped with `--skip 'TestGatewayUnixFSFileRanges'` [#213](https://github.com/ipfs/gateway-conformance/pull/213)
10+
- Updated dependencies [#236](https://github.com/ipfs/gateway-conformance/pull/236) & [#239](https://github.com/ipfs/gateway-conformance/pull/239)
11+
712
## [0.7.1] - 2025-01-03
813
### Changed
914
- Expect all URL escapes to use uppercase hex [#232](https://github.com/ipfs/gateway-conformance/pull/232)

tests/path_gateway_unixfs_test.go

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ func TestGatewaySymlink(t *testing.T) {
477477
func TestGatewayUnixFSFileRanges(t *testing.T) {
478478
tooling.LogTestGroup(t, GroupUnixFS)
479479

480-
// Multi-range requests MUST conform to the HTTP semantics. The server does not
480+
// Range requests MUST conform to the HTTP semantics. The server does not
481481
// need to be able to support returning multiple ranges. However, it must respond
482482
// correctly.
483483
fixture := car.MustOpenUnixfsCar("path_gateway_unixfs/dir-with-files.car")
@@ -488,8 +488,22 @@ func TestGatewayUnixFSFileRanges(t *testing.T) {
488488
)
489489

490490
RunWithSpecs(t, SugarTests{
491+
{
492+
Name: "GET for /ipfs/ file includes Accept-Ranges header",
493+
Hint: "Gateway returns explicit hint that range requests are supported. This is important for interop with HTTP reverse proxies, CDNs, caches.",
494+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#accept-ranges-response-header",
495+
Request: Request().
496+
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()),
497+
Response: Expect().
498+
Status(200).
499+
Headers(
500+
Header("Accept-Ranges").Equals("bytes"),
501+
).
502+
Body(fixture.MustGetRawData("ascii.txt")),
503+
},
491504
{
492505
Name: "GET for /ipfs/ file with single range request includes correct bytes",
506+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
493507
Request: Request().
494508
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
495509
Headers(
@@ -498,13 +512,30 @@ func TestGatewayUnixFSFileRanges(t *testing.T) {
498512
Response: Expect().
499513
Status(206).
500514
Headers(
501-
Header("Content-Type").Contains("text/plain"),
502515
Header("Content-Range").Equals("bytes 6-16/31"),
503516
).
504517
Body(fixture.MustGetRawData("ascii.txt")[6:17]),
505518
},
506519
{
507-
Name: "GET for /ipfs/ file with multiple range request includes correct bytes",
520+
Name: "GET for /ipfs/ file with suffix range request includes correct bytes from the end of file",
521+
Hint: "Ensures it is possible to read the tail of a file",
522+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
523+
Request: Request().
524+
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
525+
Headers(
526+
Header("Range", "bytes=-3"),
527+
),
528+
Response: Expect().
529+
Status(206).
530+
Headers(
531+
Header("Content-Range").Equals("bytes 28-30/31"),
532+
).
533+
Body(fixture.MustGetRawData("ascii.txt")[28:31]),
534+
},
535+
{
536+
Name: "GET for /ipfs/ file with multiple range request returned HTTP 206",
537+
Hint: "This test reads Content-Type and Content-Range of response, which enable later tests to check if response was acceptable (either single range, or multiple ones)",
538+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
508539
Request: Request().
509540
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
510541
Headers(
@@ -515,13 +546,16 @@ func TestGatewayUnixFSFileRanges(t *testing.T) {
515546
Headers(
516547
Header("Content-Type").
517548
Checks(func(v string) bool {
549+
// Not really a test, just inspect value
518550
contentType = v
519-
return v != ""
551+
return true
520552
}),
521553
Header("Content-Range").
522554
ChecksAll(func(v []string) bool {
555+
// Not really a test, just inspect value
523556
if len(v) == 1 {
524557
contentRange = v[0]
558+
525559
}
526560
return true
527561
}),
@@ -531,49 +565,31 @@ func TestGatewayUnixFSFileRanges(t *testing.T) {
531565

532566
tests := SugarTests{}
533567

534-
if strings.Contains(contentType, "text/plain") {
535-
// The server is not able to respond to a multi-range request. Therefore,
536-
// there might be only one range or... just the whole file, depending on the headers.
568+
multipleRangeSupported := strings.Contains(contentType, "multipart/byteranges")
569+
onlySingleRangeSupported := !multipleRangeSupported && contentRange != ""
537570

538-
if contentRange == "" {
539-
// Server does not support range requests and must send back the complete file.
540-
tests = append(tests, SugarTest{
541-
Name: "GET for /ipfs/ file with multiple range request includes correct bytes",
542-
Request: Request().
543-
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
544-
Headers(
545-
Header("Range", "bytes=6-16,0-4"),
546-
),
547-
Response: Expect().
548-
Status(206).
549-
Headers(
550-
Header("Content-Type").Contains("text/plain"),
551-
Header("Content-Range").IsEmpty(),
552-
).
553-
Body(fixture.MustGetRawData("ascii.txt")),
554-
})
555-
} else {
556-
// Server supports range requests but only the first range.
557-
tests = append(tests, SugarTest{
558-
Name: "GET for /ipfs/ file with multiple range request includes correct bytes",
559-
Request: Request().
560-
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
561-
Headers(
562-
Header("Range", "bytes=6-16,0-4"),
563-
),
564-
Response: Expect().
565-
Status(206).
566-
Headers(
567-
Header("Content-Type").Contains("text/plain"),
568-
Header("Content-Range", "bytes 6-16/31"),
569-
).
570-
Body(fixture.MustGetRawData("ascii.txt")[6:17]),
571-
})
572-
}
573-
} else if strings.Contains(contentType, "multipart/byteranges") {
571+
if onlySingleRangeSupported {
572+
// Server supports range requests but only the first range.
573+
tests = append(tests, SugarTest{
574+
Name: "GET for /ipfs/ file with multiple range request returns correct bytes for the first range",
575+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
576+
Request: Request().
577+
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
578+
Headers(
579+
Header("Range", "bytes=6-16,0-4"),
580+
),
581+
Response: Expect().
582+
Status(206).
583+
Headers(
584+
Header("Content-Range", "bytes 6-16/31"),
585+
).
586+
Body(fixture.MustGetRawData("ascii.txt")[6:17]),
587+
})
588+
} else if multipleRangeSupported {
574589
// The server supports responding with multi-range requests.
575590
tests = append(tests, SugarTest{
576591
Name: "GET for /ipfs/ file with multiple range request includes correct bytes",
592+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
577593
Request: Request().
578594
Path("/ipfs/{{cid}}/ascii.txt", fixture.MustGetCid()).
579595
Headers(
@@ -586,16 +602,48 @@ func TestGatewayUnixFSFileRanges(t *testing.T) {
586602
).
587603
Body(And(
588604
Contains("Content-Range: bytes 6-16/31"),
589-
Contains("Content-Type: text/plain"),
590605
Contains(string(fixture.MustGetRawData("ascii.txt")[6:17])),
591606
Contains("Content-Range: bytes 0-4/31"),
592607
Contains(string(fixture.MustGetRawData("ascii.txt")[0:5])),
593608
)),
594609
})
595610
} else {
596-
t.Error("Content-Type header did not match any of the accepted options")
611+
t.Error("Content-Range and Content-Type header did not match any of the accepted options for a Range request (neither single or multiple ranges are supported)")
597612
}
598613

614+
// Range request should work when unrelated parts of DAG not available.
615+
missingBlockFixture := car.MustOpenUnixfsCar("trustless_gateway_car/file-3k-and-3-blocks-missing-block.car")
616+
tests = append(tests, SugarTest{
617+
Name: "GET Range of file succeeds even if the gateway is missing a block AFTER the requested range",
618+
Hint: "This MUST succeed despite the fact that bytes beyond the end of range are not retrievable",
619+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
620+
Request: Request().
621+
Path("/ipfs/{{cid}}", missingBlockFixture.MustGetCidWithCodec(0x70)).
622+
Headers(
623+
Header("Range", "bytes=997-1000"),
624+
),
625+
Response: Expect().
626+
Status(206).
627+
Headers(
628+
Header("Content-Range").Equals("bytes 997-1000/3072"),
629+
),
630+
}, SugarTest{
631+
Name: "GET Range of file succeeds even if the gateway is missing a block BEFORE the requested range",
632+
Hint: "This MUST succeed despite the fact that bytes beyond the end of range are not retrievable",
633+
Spec: "https://specs.ipfs.tech/http-gateways/path-gateway/#range-request-header",
634+
Request: Request().
635+
Path("/ipfs/{{cid}}", missingBlockFixture.MustGetCidWithCodec(0x70)).
636+
Headers(
637+
Header("Range", "bytes=2200-2201"),
638+
),
639+
Response: Expect().
640+
Status(206).
641+
Headers(
642+
Header("Content-Range").Equals("bytes 2200-2201/3072"),
643+
),
644+
},
645+
)
646+
599647
RunWithSpecs(t, tests, specs.PathGatewayUnixFS)
600648
}
601649

tooling/check/check.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"regexp"
99
"strings"
10+
"unicode/utf8"
1011

1112
"github.com/ipfs/gateway-conformance/tooling/tmpl"
1213
)
@@ -157,9 +158,18 @@ func (c CheckIsEqualBytes) Check(v []byte) CheckOutput {
157158
}
158159
}
159160

161+
var reason string
162+
if utf8.Valid(v) && utf8.Valid(c.Value) {
163+
// Print human-readable plain text, when possible
164+
reason = fmt.Sprintf("expected %q, got %q", c.Value, v)
165+
} else {
166+
// Print byte codes
167+
reason = fmt.Sprintf("expected '%v', got '%v'", c.Value, v)
168+
}
169+
160170
return CheckOutput{
161171
Success: false,
162-
Reason: fmt.Sprintf("expected '%v', got '%v'", c.Value, v),
172+
Reason: reason,
163173
}
164174
}
165175

0 commit comments

Comments
 (0)