diff --git a/packages/interop/src/fixtures/data/gateway-conformance-fixtures.car b/packages/interop/src/fixtures/data/gateway-conformance-fixtures.car new file mode 100644 index 00000000..43e570e1 Binary files /dev/null and b/packages/interop/src/fixtures/data/gateway-conformance-fixtures.car differ diff --git a/packages/interop/src/unixfs-dir.spec.ts b/packages/interop/src/unixfs-dir.spec.ts index 2f2db818..94d109c4 100644 --- a/packages/interop/src/unixfs-dir.spec.ts +++ b/packages/interop/src/unixfs-dir.spec.ts @@ -26,6 +26,25 @@ describe('@helia/verified-fetch - unixfs directory', () => { await verifiedFetch.stop() }) + describe('unixfs-dir-redirect', () => { + before(async () => { + await loadFixtureDataCar(controller, 'gateway-conformance-fixtures.car') + }); + + [ + 'https://example.com/ipfs/bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q', + 'ipfs://bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q', + 'http://example.com/ipfs/bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q' + ].forEach((url: string) => { + it(`request to unixfs directory with ${url} should return a 301 with a trailing slash`, async () => { + const response = await verifiedFetch(url, { redirect: 'manual' }) + expect(response).to.be.ok() + expect(response.status).to.equal(301) + expect(response.headers.get('location')).to.equal(`${url}/`) + }) + }) + }) + describe('XKCD Barrel Part 1', () => { before(async () => { // This is the content of https://explore.ipld.io/#/explore/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm/1%20-%20Barrel%20-%20Part%201 diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 4166d914..304ad6f6 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -299,9 +299,10 @@ export class VerifiedFetch { let resolvedCID = terminalElement?.cid ?? cid if (terminalElement?.type === 'directory') { const dirCid = terminalElement.cid + const redirectCheckNeeded = path === '' ? !resource.toString().endsWith('/') : !path.endsWith('/') // https://specs.ipfs.tech/http-gateways/path-gateway/#use-in-directory-url-normalization - if (path !== '' && !path.endsWith('/')) { + if (redirectCheckNeeded) { if (options?.redirect === 'error') { this.log('could not redirect to %s/ as redirect option was set to "error"', resource) throw new TypeError('Failed to fetch') diff --git a/packages/verified-fetch/test/verified-fetch.spec.ts b/packages/verified-fetch/test/verified-fetch.spec.ts index bf0263af..64cbc84e 100644 --- a/packages/verified-fetch/test/verified-fetch.spec.ts +++ b/packages/verified-fetch/test/verified-fetch.spec.ts @@ -160,6 +160,33 @@ describe('@helia/verifed-fetch', () => { expect(ipfsResponse.url).to.equal(`ipfs://${res.cid}/foo`) }) + it('should return a 301 with a trailing slash when a root directory is requested without a trailing slash', async () => { + const finalRootFileContent = new Uint8Array([0x01, 0x02, 0x03]) + + const fs = unixfs(helia) + const res = await last(fs.addAll([{ + path: '/index.html', + content: finalRootFileContent + }], { + wrapWithDirectory: true + })) + + if (res == null) { + throw new Error('Import failed') + } + + const stat = await fs.stat(res.cid) + expect(stat.type).to.equal('directory') + + const ipfsResponse = await verifiedFetch.fetch(`https://ipfs.local/ipfs/${res.cid}`, { + redirect: 'manual' + }) + expect(ipfsResponse).to.be.ok() + expect(ipfsResponse.status).to.equal(301) + expect(ipfsResponse.headers.get('location')).to.equal(`https://ipfs.local/ipfs/${res.cid}/`) + expect(ipfsResponse.url).to.equal(`https://ipfs.local/ipfs/${res.cid}`) + }) + it('should return a 301 with a trailing slash when a gateway directory is requested without a trailing slash', async () => { const finalRootFileContent = new Uint8Array([0x01, 0x02, 0x03])