Skip to content

Commit f766b67

Browse files
authored
feat: relax CAR tests to accept both 200 and 404 for non-existing paths (#244)
* feat: relax CAR tests to accept both 200 and 404 for non-existing paths The response code for non-existing paths now depends on implementation details such as locality and cost of path traversal checks. - Implementations that can efficiently detect non-existing paths should return 404 Not Found (improved behavior per ipfs/boxo#458) - Implementations focusing on stateless streaming and low latency may return 200 with partial CAR up to the missing link (legacy behavior) This change: - Updates TestTrustlessCarPathing to use AnyOf for non-existing file test - Enhances applyStandardCarResponseHeaders to handle AnyOfExpectBuilder - Applies CAR headers only to 200 responses, not to error responses - Refactors helper code to be more DRY and idiomatic * chore: release v0.8.2 Includes relaxed CAR tests that accept both 200 and 404 for non-existing paths. See #244 for details.
1 parent 1a013ba commit f766b67

File tree

3 files changed

+57
-23
lines changed

3 files changed

+57
-23
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ 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.2] - 2025-08-31
8+
### Changed
9+
- Relaxed CAR tests to accept both HTTP 200 and 404 for non-existing paths. The response code now depends on implementation details such as locality and cost of path traversal checks. Implementations that can efficiently detect non-existing paths should return 404 (improved behavior per [ipfs/boxo#458](https://github.com/ipfs/boxo/issues/458)). Implementations focusing on stateless streaming and low latency may return 200 with partial CAR up to the missing link (legacy behavior). [#244](https://github.com/ipfs/gateway-conformance/pull/244)
10+
711
## [0.8.1] - 2025-06-17
812
### Changed
913
- DAG-CBOR HTML preview pages previously had to be returned without Cache-Control headers. Now they can use Cache-Control headers similar to those used in generated UnixFS directory listings. [#241](https://github.com/ipfs/gateway-conformance/pull/241)

tests/trustless_gateway_car_test.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,26 +94,35 @@ func TestTrustlessCarPathing(t *testing.T) {
9494
{
9595
Name: "GET default CAR response for non-existing file",
9696
Hint: `
97-
CAR stream of a non-existing path must return 200 OK and all the blocks necessary
98-
to traverse the path up to and including the parent of the first non-existing
99-
segment of the path, in order to allow the client to verify that the request
100-
path does not exist.
97+
The response code depends on implementation details such as the locality and the cost of path traversal checks,
98+
and trade-off between latency and correctness.
99+
Implementations that are able to efficiently detect requested content path does not exist,
100+
should not return CAR response, but a simple 404.
101+
Implementations that are focusing on stateless streaming and low latency are free to return
102+
partial CAR up to the missing link (blocks necessary to traverse the path up to and including
103+
the parent of the first non-existing segment).
101104
`,
102105
Request: Request().
103106
Path("/ipfs/{{cid}}/subdir/i-do-not-exist", subdirTwoSingleBlockFilesFixture.MustGetCidWithCodec(0x70)).
104107
Query("format", "car"),
105-
Response: Expect().
106-
Status(200).
107-
Body(
108-
IsCar().
109-
IgnoreRoots().
110-
HasBlocks(
111-
subdirTwoSingleBlockFilesFixture.MustGetCid(),
112-
subdirTwoSingleBlockFilesFixture.MustGetCid("subdir"),
113-
).
114-
Exactly().
115-
InThatOrder(),
116-
),
108+
Response: AnyOf(
109+
// Stateless streaming implementations: 200 with partial CAR
110+
Expect().
111+
Status(200).
112+
Body(
113+
IsCar().
114+
IgnoreRoots().
115+
HasBlocks(
116+
subdirTwoSingleBlockFilesFixture.MustGetCid(),
117+
subdirTwoSingleBlockFilesFixture.MustGetCid("subdir"),
118+
).
119+
Exactly().
120+
InThatOrder(),
121+
),
122+
// Implementations with efficient path checks: 404 Not Found
123+
Expect().
124+
Status(404),
125+
),
117126
},
118127
}
119128

tooling/helpers/car.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,9 @@ func StandardCARTestTransforms(t *testing.T, sts test.SugarTests) test.SugarTest
1717
return out
1818
}
1919

20-
func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.SugarTest {
21-
resp, ok := st.Response.(test.ExpectBuilder)
22-
if !ok {
23-
t.Fatal("can only apply test transformation on an ExpectBuilder")
24-
}
25-
st.Response = resp.Headers(
20+
// carResponseHeaders returns the standard headers expected for CAR responses
21+
func carResponseHeaders() []test.HeaderBuilder {
22+
return []test.HeaderBuilder{
2623
// TODO: Go always sends Content-Length and it's not possible to explicitly disable the behavior.
2724
// For now, we ignore this check. It should be able to be resolved soon: https://github.com/ipfs/boxo/pull/177
2825
// test.Header("Content-Length").
@@ -43,7 +40,31 @@ func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.Sugar
4340
test.Header("Etag").
4441
Hint("Etag must be present for caching purposes").
4542
Not().IsEmpty(),
46-
)
43+
}
44+
}
45+
46+
func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.SugarTest {
47+
switch resp := st.Response.(type) {
48+
case test.AnyOfExpectBuilder:
49+
// Apply headers only to successful CAR responses (status 200)
50+
transformedExpects := make([]test.ExpectBuilder, 0, len(resp.Expect_))
51+
for _, expect := range resp.Expect_ {
52+
// Only apply CAR headers to 200 responses
53+
// 404/410 responses don't have CAR content, so no CAR headers needed
54+
if expect.StatusCode_ == 200 {
55+
expect = expect.Headers(carResponseHeaders()...)
56+
}
57+
transformedExpects = append(transformedExpects, expect)
58+
}
59+
st.Response = test.AnyOf(transformedExpects...)
60+
61+
case test.ExpectBuilder:
62+
st.Response = resp.Headers(carResponseHeaders()...)
63+
64+
default:
65+
t.Fatal("can only apply test transformation on an ExpectBuilder or AnyOfExpectBuilder")
66+
}
67+
4768
return st
4869
}
4970

0 commit comments

Comments
 (0)