Skip to content

Commit ea9e4f4

Browse files
authored
fix: parse path/subdomain gateway url as ipfs/ipns url (#946)
Converts path/subdomain gateway urls to ipfs/ipns urls before handing off to verified fetch. Enables cache and directory listing conformance tests.
1 parent 39dce78 commit ea9e4f4

16 files changed

+312
-51
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@helia/http": "^3.0.8",
6363
"@helia/interface": "^6.0.2",
6464
"@helia/routers": "^4.0.3",
65-
"@helia/verified-fetch": "^5.1.0",
65+
"@helia/verified-fetch": "^6.0.0",
6666
"@ipld/dag-cbor": "^9.2.5",
6767
"@ipld/dag-json": "^10.2.5",
6868
"@ipld/dag-pb": "^4.1.5",

src/sw/handlers/content-request-handler.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isBitswapProvider, isTrustlessGatewayProvider } from '../../lib/provide
1212
import { APP_NAME, APP_VERSION, GIT_REVISION } from '../../version.js'
1313
import { getConfig } from '../lib/config.js'
1414
import { getInstallTime } from '../lib/install-time.js'
15+
import { httpResourceToIpfsUrl } from '../lib/resource-to-url.ts'
1516
import { getVerifiedFetch } from '../lib/verified-fetch.js'
1617
import { fetchErrorPageResponse } from '../pages/fetch-error-page.js'
1718
import { originIsolationWarningPageResponse } from '../pages/origin-isolation-warning-page.js'
@@ -223,11 +224,27 @@ async function fetchHandler ({ url, headers, renderPreview, event, logs, subdoma
223224
otherCount: 0
224225
}
225226

226-
const resource = url.href
227+
const resource = httpResourceToIpfsUrl(url)
228+
229+
// check for redirect
230+
if (resource instanceof Response) {
231+
return resource
232+
}
227233

228234
const firstInstallTime = await getInstallTime()
229235
const start = Date.now()
230236

237+
let ifNoneMatch = headers.get('if-none-match')
238+
239+
// these tokens are added to the header by the entity renderer response,
240+
// remove them for internal comparison by verified fetch
241+
if (ifNoneMatch != null) {
242+
ifNoneMatch = ifNoneMatch.replace('DirIndex-.*_CID-', '')
243+
ifNoneMatch = ifNoneMatch.replace('DagIndex-', '')
244+
245+
headers.set('if-none-match', ifNoneMatch)
246+
}
247+
231248
/**
232249
* Note that there are existing bugs regarding service worker signal handling:
233250
* https://bugs.chromium.org/p/chromium/issues/detail?id=823697
@@ -359,7 +376,8 @@ async function fetchHandler ({ url, headers, renderPreview, event, logs, subdoma
359376
status: 500,
360377
statusText: 'Internal Server Error',
361378
headers: {
362-
'content-type': 'application/json'
379+
'content-type': 'application/json',
380+
'x-error-message': btoa(err.message)
363381
}
364382
}), JSON.stringify(errorToObject(err), null, 2), providers, config, firstInstallTime, logs)
365383
} finally {
@@ -384,10 +402,6 @@ function shouldRenderDirectory (url: URL, config: ConfigDb, accept?: string | nu
384402
return true
385403
}
386404

387-
if (config.renderHTMLViews === false) {
388-
return false
389-
}
390-
391405
return accept?.includes('text/html') === true
392406
}
393407

src/sw/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { APP_NAME, APP_VERSION, GIT_REVISION } from '../version.js'
66
import { handlers } from './handlers/index.js'
77
import { getConfig } from './lib/config.js'
88
import { getInstallTime, setInstallTime } from './lib/install-time.js'
9+
import { updateRedirect } from './lib/update-redirect.ts'
910
import { serverErrorPageResponse } from './pages/server-error-page.js'
1011

1112
/**
@@ -116,6 +117,10 @@ self.addEventListener('fetch', (event) => {
116117
)
117118
}
118119

120+
// if verified-fetch has redirected us, update the location header in
121+
// the response to be a HTTP location not ipfs/ipns
122+
updateRedirect(url, response)
123+
119124
// add the server header to the response so we can be sure this response
120125
// came from the service worker - sometimes these are read-only so we
121126
// cannot just `response.headers.set('server', ...)`

src/sw/lib/resource-to-url.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { InvalidParametersError } from '@libp2p/interface'
2+
import { peerIdFromString } from '@libp2p/peer-id'
3+
4+
export const SUBDOMAIN_GATEWAY_REGEX = /^(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.(?<host>[^/?]+)$/
5+
6+
export interface SubdomainMatchGroups {
7+
protocol: 'ipfs' | 'ipns'
8+
cidOrPeerIdOrDnsLink: string
9+
host: string
10+
}
11+
12+
export function matchSubdomainGroupsGuard (groups?: null | { [key in string]: string; } | SubdomainMatchGroups): groups is SubdomainMatchGroups {
13+
const protocol = groups?.protocol
14+
15+
if (protocol !== 'ipfs' && protocol !== 'ipns') {
16+
return false
17+
}
18+
19+
const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
20+
21+
if (cidOrPeerIdOrDnsLink == null) {
22+
return false
23+
}
24+
25+
return true
26+
}
27+
28+
// DNS label can have up to 63 characters, consisting of alphanumeric
29+
// characters or hyphens -, but it must not start or end with a hyphen.
30+
const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
31+
32+
/**
33+
* Checks if label looks like inlined DNSLink.
34+
* (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header)
35+
*/
36+
function isInlinedDnsLink (label: string): boolean {
37+
return dnsLabelRegex.test(label) && label.includes('-') && !label.includes('.')
38+
}
39+
40+
/**
41+
* DNSLink label decoding
42+
* - Every standalone - is replaced with .
43+
* - Every remaining -- is replaced with -
44+
*
45+
* @example en-wikipedia--on--ipfs-org -> en.wikipedia-on-ipfs.org
46+
*/
47+
export function decodeDNSLinkLabel (label: string): string {
48+
return label.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
49+
}
50+
51+
/**
52+
* If the caller has passed a case-sensitive identifier (like a base58btc
53+
* encoded CID or PeerId) in a case-insensitive location (like a subdomain),
54+
* be nice and return the original identifier from the passed string
55+
*/
56+
function findOriginalCidOrPeer (needle: string, haystack: URL): string {
57+
const start = haystack.href.toLowerCase().indexOf(needle)
58+
59+
if (start === -1) {
60+
return needle
61+
}
62+
63+
return haystack.href.substring(start, start + needle.length)
64+
}
65+
66+
/**
67+
* Takes a subdomain or path gateway URL and turns it into an IPFS/IPNS URL, or
68+
* a redirect response that directs the user to a canonical URL for the resource
69+
*/
70+
export function httpResourceToIpfsUrl (resource: URL): URL | Response {
71+
// test for subdomain gateway URL - match hostname to exclude port
72+
const subdomainMatch = resource.hostname.match(SUBDOMAIN_GATEWAY_REGEX)
73+
74+
if (matchSubdomainGroupsGuard(subdomainMatch?.groups)) {
75+
const groups = subdomainMatch.groups
76+
77+
if (groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
78+
// decode inline dnslink domain if present
79+
groups.cidOrPeerIdOrDnsLink = decodeDNSLinkLabel(groups.cidOrPeerIdOrDnsLink)
80+
}
81+
82+
const cidOrPeerIdOrDnsLink = findOriginalCidOrPeer(groups.cidOrPeerIdOrDnsLink, resource)
83+
84+
// parse url as not http(s):// - this is necessary because URL makes
85+
// `.pathname` default to `/` for http URLs, even if no trailing slash was
86+
// present in the string URL and we need to be able to round-trip the user's
87+
// input while also maintaining a sane canonical URL for the resource. Phew.
88+
const wat = new URL(`not-${resource}`)
89+
90+
return new URL(`${groups.protocol}://${cidOrPeerIdOrDnsLink}${wat.pathname}${resource.search}${resource.hash}`)
91+
}
92+
93+
// test for IPFS path gateway URL
94+
if (resource.pathname.startsWith('/ipfs/')) {
95+
const parts = resource.pathname.substring(6).split('/')
96+
const cid = parts.shift()
97+
98+
if (cid == null) {
99+
throw new InvalidParametersError(`Path gateway URL ${resource} had no CID`)
100+
}
101+
102+
return new URL(`ipfs://${cid}${resource.pathname.replace(`/ipfs/${cid}`, '')}${resource.search}${resource.hash}`)
103+
}
104+
105+
// test for IPNS path gateway URL
106+
if (resource.pathname.startsWith('/ipns/')) {
107+
const parts = resource.pathname.substring(6).split('/')
108+
let name = parts.shift()
109+
110+
if (name == null) {
111+
throw new InvalidParametersError(`Path gateway URL ${resource} had no name`)
112+
}
113+
114+
// special case - re-encode base58btc IPNS name as base36 CID and redirect
115+
// @see TestRedirectCanonicalIPNS/GET_for_%2Fipns%2F%7Bb58-multihash-of-ed25519-key%7D_redirects_to_%2Fipns%2F%7Bcidv1-libp2p-key-base36%7D
116+
if (name.startsWith('12D3K')) {
117+
const peerId = peerIdFromString(name)
118+
name = peerId.toCID().toString()
119+
120+
return new Response('', {
121+
status: 301,
122+
headers: {
123+
location: `/ipns/${name}/${parts.join('/')}${resource.search}${resource.hash}`
124+
}
125+
})
126+
}
127+
128+
return new URL(`ipns://${name}${resource.pathname.replace(`/ipns/${name}`, '')}${resource.search}${resource.hash}`)
129+
}
130+
131+
throw new TypeError(`Invalid URL: ${resource}, please use subdomain or path gateway URLs only`)
132+
}

src/sw/lib/update-redirect.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { matchSubdomainGroupsGuard, SUBDOMAIN_GATEWAY_REGEX } from './resource-to-url.ts'
2+
3+
/**
4+
* If the response has a location header with an ipfs/ipns URL, translate it
5+
* into a HTTP URL that a browser can use
6+
*/
7+
export function updateRedirect (resource: URL, response: Response): Response {
8+
let location = response.headers.get('location')
9+
10+
if (location == null || location.trim() === '') {
11+
return response
12+
}
13+
14+
if (location.startsWith('?') || location.startsWith('/') || location.startsWith('#')) {
15+
// partial location, prefix with current origin
16+
location = `${resource.href}${location}`
17+
}
18+
19+
const url = new URL(location)
20+
21+
if (url.protocol.startsWith('http')) {
22+
return response
23+
}
24+
25+
// match host to include port (if present)
26+
const subdomainMatch = resource.host.match(SUBDOMAIN_GATEWAY_REGEX)
27+
28+
if (matchSubdomainGroupsGuard(subdomainMatch?.groups)) {
29+
const { host } = subdomainMatch.groups
30+
31+
location = `${resource.protocol}//${url.hostname}.${url.protocol.replace(':', '')}.${host}${url.pathname}${url.search}${url.hash}`
32+
} else {
33+
location = `${resource.protocol}//${resource.host}/${url.protocol.replace(':', '')}/${url.hostname}${url.pathname}${url.search}${url.hash}`
34+
}
35+
36+
response.headers.set('location', location)
37+
38+
return response
39+
}

src/sw/pages/fetch-error-page.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ function toErrorPageProviders (providers: Providers): ErrorPageProviders {
5858
}
5959

6060
export interface RequestDetails {
61-
resource: string
61+
resource: URL
6262
method: string
6363
headers: Record<string, string>
6464
}
6565

66-
function getRequestDetails (resource: string, init: RequestInit): RequestDetails {
66+
function getRequestDetails (resource: URL, init: RequestInit): RequestDetails {
6767
const requestHeaders = new Headers(init.headers)
6868
const headers: Record<string, string> = {}
6969
requestHeaders.forEach((value, key) => {
@@ -103,7 +103,7 @@ function getResponseDetails (response: Response, body: string): ResponseDetails
103103
/**
104104
* Shows an error page to the user
105105
*/
106-
export function fetchErrorPageResponse (resource: string, request: RequestInit, fetchResponse: Response, responseBody: string, providers: Providers, config: ConfigDb, installTime: number, logs: string[]): Response {
106+
export function fetchErrorPageResponse (resource: URL, request: RequestInit, fetchResponse: Response, responseBody: string, providers: Providers, config: ConfigDb, installTime: number, logs: string[]): Response {
107107
const responseContentType = fetchResponse.headers.get('Content-Type')
108108

109109
if (responseContentType?.includes('text/html')) {

src/sw/pages/render-entity.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MEDIA_TYPE_DAG_PB } from '@helia/verified-fetch'
12
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
23
import { headersToObject } from '../../lib/headers-to-object.ts'
34
import { APP_NAME, APP_VERSION, GIT_REVISION } from '../../version.js'
@@ -33,7 +34,11 @@ export function renderEntityPageResponse (url: URL, headers: Headers, response:
3334
const page = htmlPage(props.cid ?? '', 'renderEntity', props)
3435
mergedHeaders.set('content-length', `${page.length}`)
3536

36-
mergedHeaders.set('etag', `"DagIndex-${props.cid}.html"`)
37+
if (contentType === MEDIA_TYPE_DAG_PB) {
38+
mergedHeaders.set('etag', `"DirIndex-.*_CID-${props.cid}"`)
39+
} else {
40+
mergedHeaders.set('etag', `"DagIndex-${props.cid}"`)
41+
}
3742

3843
return new Response(page, {
3944
status: response.status,

src/ui/utils/links.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export function createLink ({ ipfsPath, params, hash }: CreateLinkOptions): stri
3838
if (isSubdomain) {
3939
const host = (url.host.includes('.ipfs.') ? url.host.split('.ipfs.') : url.host.split('.ipns.'))[1]
4040

41-
return `${url.protocol}//${CID.parse(cid).toV1()}.ipfs.${host}${path === '/' ? '' : path}${search}${hash ?? url.hash}`
41+
try {
42+
return `${url.protocol}//${CID.parse(cid).toV1()}.ipfs.${host}${path === '/' ? '' : path}${search}${hash ?? url.hash}`
43+
} catch {
44+
return `${url.protocol}//${cid}.ipns.${host}${path === '/' ? '' : path}${search}${hash ?? url.hash}`
45+
}
4246
}
4347

4448
return `${url.protocol}//${url.host}/ipfs/${cid}${path === '/' ? '' : path}${search}${hash ?? url.hash}`

test-conformance/fixtures/expected-passing-tests.json

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@
387387
"TestGatewayIPNSPath/GET_for_%2Fipns%2Fname_with_valid_V2-only_signature_succeeds",
388388
"TestGatewayIPNSPath/GET_for_%2Fipns%2Fname_with_valid_V2-only_signature_succeeds/Body",
389389
"TestGatewayIPNSPath/GET_for_%2Fipns%2Fname_with_valid_V1_and_broken_V2_signature_MUST_fail_with_5XX",
390-
"TestRedirectCanonicalIPNS",
391390
"TestGatewayBlock",
392391
"TestGatewayBlock/GET_with_format=raw_param_returns_a_raw_block",
393392
"TestGatewayBlock/GET_with_format=raw_param_returns_a_raw_block/Status_code",
@@ -441,6 +440,27 @@
441440
"TestUnixFSDirectoryListing/GET_for_%2Fipfs%2Fcid%2Ffile_UnixFS_file_that_does_not_exist_returns_404",
442441
"TestUnixFSDirectoryListing/GET_for_%2Fipfs%2Fcid%2Ffile_UnixFS_file_that_does_not_exist_returns_404/Status_code",
443442
"TestGatewayCache",
443+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds",
444+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_0",
445+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_0/Status_code",
446+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_0/Header_X-Ipfs-Path",
447+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_0/Header_X-Ipfs-Roots",
448+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_0/Header_Etag",
449+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_1",
450+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_1/Check_0",
451+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_listing_succeeds/Check_1/Check_1",
452+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds",
453+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds/Status_code",
454+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds/Header_Cache-Control",
455+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds/Header_X-Ipfs-Path",
456+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds/Header_X-Ipfs-Roots",
457+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_with_index.html_succeeds/Header_Etag",
458+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds",
459+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds/Status_code",
460+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds/Header_Cache-Control",
461+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds/Header_X-Ipfs-Path",
462+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds/Header_X-Ipfs-Roots",
463+
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_file_succeeds/Header_Etag",
444464
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_as_DAG-JSON_succeeds",
445465
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_as_DAG-JSON_succeeds/Status_code",
446466
"TestGatewayCache/GET_for_%2Fipfs%2F_unixfs_dir_as_DAG-JSON_succeeds/Header_Cache-Control",
@@ -461,7 +481,39 @@
461481
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_matching_weak_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
462482
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_wildcard_Etag_in_If-None-Match_returns_304_Not_Modified",
463483
"TestGatewayCache/GET_for_%2Fipfs%2F_file_with_wildcard_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
484+
"TestGatewayCache/DirIndex_etag_is_based_on_xxhash%28.%2Fassets%2Fdir-index-html%29%2C_so_we_need_to_fetch_it_dynamically",
485+
"TestGatewayCache/DirIndex_etag_is_based_on_xxhash%28.%2Fassets%2Fdir-index-html%29%2C_so_we_need_to_fetch_it_dynamically/Status_code",
486+
"TestGatewayCache/DirIndex_etag_is_based_on_xxhash%28.%2Fassets%2Fdir-index-html%29%2C_so_we_need_to_fetch_it_dynamically/Header_Etag",
487+
"TestGatewayCache/GET_for_%2Fipfs%2F_dir_listing_with_matching_strong_Etag_in_If-None-Match_returns_304_Not_Modified",
488+
"TestGatewayCache/GET_for_%2Fipfs%2F_dir_listing_with_matching_strong_Etag_in_If-None-Match_returns_304_Not_Modified/Status_code",
464489
"TestGatewayCacheWithIPNS",
490+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds",
491+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_0",
492+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_0/Status_code",
493+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_0/Header_X-Ipfs-Path",
494+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_0/Header_X-Ipfs-Roots",
495+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_0/Header_Etag",
496+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_1",
497+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_1/Check_0",
498+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_listing_succeeds/Check_1/Check_1",
499+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds",
500+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_0",
501+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_0/Status_code",
502+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_0/Header_X-Ipfs-Path",
503+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_0/Header_X-Ipfs-Roots",
504+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_0/Header_Etag",
505+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_1",
506+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_1/Check_0",
507+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_with_index.html_succeeds/Check_1/Check_1",
508+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds",
509+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_0",
510+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_0/Status_code",
511+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_0/Header_X-Ipfs-Path",
512+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_0/Header_X-Ipfs-Roots",
513+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_0/Header_Etag",
514+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_1",
515+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_1/Check_0",
516+
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_file_succeeds/Check_1/Check_1",
465517
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_DAG-JSON_succeeds",
466518
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_DAG-JSON_succeeds/Check_0",
467519
"TestGatewayCacheWithIPNS/GET_for_%2Fipns%2F_unixfs_dir_as_DAG-JSON_succeeds/Check_0/Status_code",

0 commit comments

Comments
 (0)