Skip to content

Commit 695fd17

Browse files
committed
test: Etag, X-Ipfs-Path and X-Ipfs-Roots
This documents the current behavior of mentioned headers. It helped to identify a a bug around index.html responses having etag of a dir-index-html (to be fixed in a separate change)
1 parent 56140cd commit 695fd17

File tree

3 files changed

+137
-21
lines changed

3 files changed

+137
-21
lines changed

core/corehttp/gateway_handler.go

+49-21
Original file line numberDiff line numberDiff line change
@@ -317,28 +317,12 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
317317
w.Header().Set("X-IPFS-Path", urlPath)
318318
w.Header().Set("Etag", responseEtag)
319319

320-
// X-Ipfs-Roots array for efficient HTTP cache invalidation
321-
// These are logical roots where each CID represent one path segment
322-
// and resolves to either a directory or the root block of a file
323-
var sp strings.Builder
324-
var pathRoots []string
325-
pathSegments := strings.Split(urlPath[6:], "/")
326-
sp.WriteString(urlPath[:5]) // /ipfs or /ipns
327-
for _, root := range pathSegments {
328-
if root == "" {
329-
continue
330-
}
331-
sp.WriteString("/")
332-
sp.WriteString(root)
333-
resolvedSubPath, err := i.api.ResolvePath(r.Context(), ipath.New(sp.String()))
334-
if err != nil {
335-
// this should never happen, as we resolved the full path already
336-
webError(w, "error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
337-
return
338-
}
339-
pathRoots = append(pathRoots, resolvedSubPath.Cid().String())
320+
if rootCids, err := i.buildIpfsRootsHeader(urlPath, r); err == nil {
321+
w.Header().Set("X-Ipfs-Roots", rootCids)
322+
} else { // this should never happen, as we resolved the urlPath already
323+
webError(w, "error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
324+
return
340325
}
341-
w.Header().Set("X-Ipfs-Roots", strings.Join(pathRoots, ", "))
342326

343327
// set these headers _after_ the error, for we may just not have it
344328
// and don't want the client to cache a 500 response...
@@ -781,6 +765,50 @@ func (i *gatewayHandler) addUserHeaders(w http.ResponseWriter) {
781765
}
782766
}
783767

768+
// Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation.
769+
func (i *gatewayHandler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) {
770+
/*
771+
These are logical roots where each CID represent one path segment
772+
and resolves to either a directory or the root block of a file.
773+
The main purpose of this header is allow HTTP caches to do smarter decisions
774+
around cache invalidation (eg. keep specific subdirectory/file if it did not change)
775+
776+
A good example is Wikipedia, which is HAMT-sharded, but we only care about
777+
logical roots that represent each segment of the human-readable content
778+
path:
779+
780+
Given contentPath = /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey
781+
rootCidList is a generated by doing `ipfs resolve -r` on each sub path:
782+
/ipns/en.wikipedia-on-ipfs.org → bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze
783+
/ipns/en.wikipedia-on-ipfs.org/wiki/ → bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4
784+
/ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey → bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
785+
786+
The result is an ordered array of values:
787+
X-Ipfs-Roots: bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze,bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4,bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
788+
789+
Note that while the top one will change every time any article is changed,
790+
the last root (responsible for specific article) may not change at all.
791+
*/
792+
var sp strings.Builder
793+
var pathRoots []string
794+
pathSegments := strings.Split(contentPath[6:], "/")
795+
sp.WriteString(contentPath[:5]) // /ipfs or /ipns
796+
for _, root := range pathSegments {
797+
if root == "" {
798+
continue
799+
}
800+
sp.WriteString("/")
801+
sp.WriteString(root)
802+
resolvedSubPath, err := i.api.ResolvePath(r.Context(), ipath.New(sp.String()))
803+
if err != nil {
804+
return "", err
805+
}
806+
pathRoots = append(pathRoots, resolvedSubPath.Cid().String())
807+
}
808+
rootCidList := strings.Join(pathRoots, ",") // convention from rfc2616#sec4.2
809+
return rootCidList, nil
810+
}
811+
784812
func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
785813
if _, ok := err.(resolver.ErrNoLink); ok {
786814
webErrorWithCode(w, message, err, http.StatusNotFound)

test/sharness/t0116-gateway-cache.sh

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
3+
test_description="Test HTTP Gateway Cache Control Support"
4+
5+
. lib/test-lib.sh
6+
7+
test_init_ipfs
8+
test_launch_ipfs_daemon_without_network
9+
10+
# Cache control support is based on logical roots (each path segment == one logical root).
11+
# To maximize the test surface, we want to test:
12+
# - /ipfs/ content path
13+
# - /ipns/ content path
14+
# - at least 3 levels
15+
# - separate tests for a directory listing and a file
16+
# - have implicit index.html for a good measure
17+
# /ipns/root1/root2/root3/ (/ipns/root1/root2/root3/index.html)
18+
19+
test_expect_success "Add the test directory" '
20+
mkdir -p root2/root3 &&
21+
echo "hello" > root2/root3/index.html &&
22+
ROOT1_CID=$(ipfs add -Qrw --cid-version 1 root2)
23+
ROOT2_CID=$(ipfs resolve -r /ipfs/$ROOT1_CID/root2 | cut -d "/" -f3)
24+
ROOT3_CID=$(ipfs resolve -r /ipfs/$ROOT1_CID/root2/root3 | cut -d "/" -f3)
25+
FILE_CID=$(ipfs resolve -r /ipfs/$ROOT1_CID/root2/root3/index.html | cut -d "/" -f3)
26+
'
27+
28+
test_expect_success "Prepare IPNS unixfs content path for testing" '
29+
TEST_IPNS_ID=$(ipfs key gen --ipns-base=base36 --type=ed25519 cache_test_key | head -n1 | tr -d "\n")
30+
ipfs name publish --key cache_test_key --allow-offline -Q "/ipfs/$ROOT1_CID" > name_publish_out &&
31+
test_check_peerid "${TEST_IPNS_ID}" &&
32+
ipfs name resolve "${TEST_IPNS_ID}" > output &&
33+
printf "/ipfs/%s\n" "$ROOT1_CID" > expected &&
34+
test_cmp expected output
35+
'
36+
37+
test_expect_success "GET for /ipfs/ unixfs dir index succeeds" '
38+
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/" >/dev/null 2>curl_ipfs_dir_output &&
39+
cat curl_ipfs_dir_output
40+
'
41+
test_expect_success "GET for /ipfs/ unixfs file succeeds" '
42+
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipfs/$ROOT1_CID/root2/root3/index.html" >/dev/null 2>curl_ipfs_file_output &&
43+
cat curl_ipfs_file_output
44+
'
45+
test_expect_success "GET for /ipns/ unixfs dir index succeeds" '
46+
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/" >/dev/null 2>curl_ipns_dir_output &&
47+
cat curl_ipns_dir_output
48+
'
49+
test_expect_success "GET for /ipns/ unixfs file succeeds" '
50+
curl -svX GET "http://127.0.0.1:$GWAY_PORT/ipns/$TEST_IPNS_ID/root2/root3/index.html" >/dev/null 2>curl_ipns_file_output &&
51+
cat curl_ipns_file_output
52+
'
53+
54+
# X-Ipfs-Path
55+
test_expect_success "GET /ipfs/ response has original content path in X-Ipfs-Path" '
56+
grep "< X-Ipfs-Path: /ipfs/$ROOT1_CID/root2/root3" curl_ipfs_dir_output
57+
'
58+
test_expect_success "GET /ipns/ response has original content path in X-Ipfs-Path" '
59+
grep "< X-Ipfs-Path: /ipns/$TEST_IPNS_ID/root2/root3" curl_ipns_dir_output
60+
'
61+
62+
# X-Ipfs-Roots
63+
test_expect_success "GET /ipfs/ response has logical CID roots in X-Ipfs-Roots" '
64+
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipfs_dir_output
65+
'
66+
test_expect_success "GET /ipns/ response has logical CID roots in X-Ipfs-Roots" '
67+
grep "< X-Ipfs-Roots: ${ROOT1_CID},${ROOT2_CID},${ROOT3_CID}" curl_ipns_dir_output
68+
'
69+
70+
# Etag for DirIndex
71+
test_expect_success "GET /ipfs/ response has special Etag for unixfs dir listing" '
72+
grep -E "< Etag: \"DirIndex-.+_CID-${ROOT3_CID}\"" curl_ipfs_dir_output
73+
'
74+
test_expect_success "GET /ipns/ response has special Etag for unixfs dir listing" '
75+
grep -E "< Etag: \"DirIndex-.+_CID-${ROOT3_CID}\"" curl_ipns_dir_output
76+
'
77+
78+
# Etag for a file
79+
test_expect_success "GET /ipfs/ response has CID as Etag for a file" '
80+
grep -E "< Etag: \"${FILE_CID}\"" curl_ipfs_file_output
81+
'
82+
test_expect_success "GET /ipns/ response has CID as Etag for a file" '
83+
grep -E "< Etag: \"${FILE_CID}\"" curl_ipns_file_output
84+
'
85+
86+
test_kill_ipfs_daemon
87+
88+
test_done
File renamed without changes.

0 commit comments

Comments
 (0)