diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad545965..80a4a4f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.2] - 2025-08-31 +### Changed +- 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) + ## [0.8.1] - 2025-06-17 ### Changed - 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) diff --git a/tests/trustless_gateway_car_test.go b/tests/trustless_gateway_car_test.go index 9f1831476..805e20692 100644 --- a/tests/trustless_gateway_car_test.go +++ b/tests/trustless_gateway_car_test.go @@ -94,26 +94,35 @@ func TestTrustlessCarPathing(t *testing.T) { { Name: "GET default CAR response for non-existing file", Hint: ` - CAR stream of a non-existing path must return 200 OK and all the blocks necessary - to traverse the path up to and including the parent of the first non-existing - segment of the path, in order to allow the client to verify that the request - path does not exist. + The response code depends on implementation details such as the locality and the cost of path traversal checks, + and trade-off between latency and correctness. + Implementations that are able to efficiently detect requested content path does not exist, + should not return CAR response, but a simple 404. + Implementations that are focusing on stateless streaming and low latency are free to return + partial CAR up to the missing link (blocks necessary to traverse the path up to and including + the parent of the first non-existing segment). `, Request: Request(). Path("/ipfs/{{cid}}/subdir/i-do-not-exist", subdirTwoSingleBlockFilesFixture.MustGetCidWithCodec(0x70)). Query("format", "car"), - Response: Expect(). - Status(200). - Body( - IsCar(). - IgnoreRoots(). - HasBlocks( - subdirTwoSingleBlockFilesFixture.MustGetCid(), - subdirTwoSingleBlockFilesFixture.MustGetCid("subdir"), - ). - Exactly(). - InThatOrder(), - ), + Response: AnyOf( + // Stateless streaming implementations: 200 with partial CAR + Expect(). + Status(200). + Body( + IsCar(). + IgnoreRoots(). + HasBlocks( + subdirTwoSingleBlockFilesFixture.MustGetCid(), + subdirTwoSingleBlockFilesFixture.MustGetCid("subdir"), + ). + Exactly(). + InThatOrder(), + ), + // Implementations with efficient path checks: 404 Not Found + Expect(). + Status(404), + ), }, } diff --git a/tooling/helpers/car.go b/tooling/helpers/car.go index b0d1f784e..92ba39020 100644 --- a/tooling/helpers/car.go +++ b/tooling/helpers/car.go @@ -17,12 +17,9 @@ func StandardCARTestTransforms(t *testing.T, sts test.SugarTests) test.SugarTest return out } -func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.SugarTest { - resp, ok := st.Response.(test.ExpectBuilder) - if !ok { - t.Fatal("can only apply test transformation on an ExpectBuilder") - } - st.Response = resp.Headers( +// carResponseHeaders returns the standard headers expected for CAR responses +func carResponseHeaders() []test.HeaderBuilder { + return []test.HeaderBuilder{ // TODO: Go always sends Content-Length and it's not possible to explicitly disable the behavior. // For now, we ignore this check. It should be able to be resolved soon: https://github.com/ipfs/boxo/pull/177 // test.Header("Content-Length"). @@ -43,7 +40,31 @@ func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.Sugar test.Header("Etag"). Hint("Etag must be present for caching purposes"). Not().IsEmpty(), - ) + } +} + +func applyStandardCarResponseHeaders(t *testing.T, st test.SugarTest) test.SugarTest { + switch resp := st.Response.(type) { + case test.AnyOfExpectBuilder: + // Apply headers only to successful CAR responses (status 200) + transformedExpects := make([]test.ExpectBuilder, 0, len(resp.Expect_)) + for _, expect := range resp.Expect_ { + // Only apply CAR headers to 200 responses + // 404/410 responses don't have CAR content, so no CAR headers needed + if expect.StatusCode_ == 200 { + expect = expect.Headers(carResponseHeaders()...) + } + transformedExpects = append(transformedExpects, expect) + } + st.Response = test.AnyOf(transformedExpects...) + + case test.ExpectBuilder: + st.Response = resp.Headers(carResponseHeaders()...) + + default: + t.Fatal("can only apply test transformation on an ExpectBuilder or AnyOfExpectBuilder") + } + return st }