From 02c3c456f05260db0e1cc6cd310b2c863ac504cc Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:03:09 -0700 Subject: [PATCH 1/6] fix: implicit accept header can be overridden by format query --- .../src/utils/select-output-type.ts | 3 +- packages/verified-fetch/src/verified-fetch.ts | 13 ++++- .../test/verified-fetch.spec.ts | 58 +++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/verified-fetch/src/utils/select-output-type.ts b/packages/verified-fetch/src/utils/select-output-type.ts index 0f7c40c..53d7632 100644 --- a/packages/verified-fetch/src/utils/select-output-type.ts +++ b/packages/verified-fetch/src/utils/select-output-type.ts @@ -55,6 +55,7 @@ const CID_TYPE_MAP: Record = { 'application/octet-stream', 'application/vnd.ipld.raw', 'application/vnd.ipfs.ipns-record', + 'application/vnd.ipld.dag-json', 'application/vnd.ipld.car', 'application/x-tar' ] @@ -145,7 +146,7 @@ function parseQFactor (str?: string): number { return factor } -const FORMAT_TO_MIME_TYPE: Record = { +export const FORMAT_TO_MIME_TYPE: Record = { raw: 'application/vnd.ipld.raw', car: 'application/vnd.ipld.car', 'dag-json': 'application/vnd.ipld.dag-json', diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 304ad6f..b9edf8a 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -25,7 +25,7 @@ import { tarStream } from './utils/get-tar-stream.js' import { parseResource } from './utils/parse-resource.js' import { setCacheControlHeader } from './utils/response-headers.js' import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js' -import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js' +import { selectOutputType, queryFormatToAcceptHeader, FORMAT_TO_MIME_TYPE } from './utils/select-output-type.js' import { walkPath } from './utils/walk-path.js' import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js' import type { RequestFormatShorthand } from './types.js' @@ -512,7 +512,16 @@ export class VerifiedFetch { this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping) } - const acceptHeader = incomingAcceptHeader ?? queryFormatMapping + let acceptHeader: string | undefined + // if the incomingAcceptHeader is autogenerated by the requesting client (browser/curl/fetch/etc) then we may need to override it if query.format is specified + if (queryFormatMapping != null && (incomingAcceptHeader == null || !Object.values(FORMAT_TO_MIME_TYPE).includes(incomingAcceptHeader))) { + this.log('accept header not recognized, but query format provided, setting accept header to %s', queryFormatMapping) + acceptHeader = queryFormatMapping + } else { + acceptHeader = incomingAcceptHeader ?? queryFormatMapping + } + this.log('determined accept header "%s"', acceptHeader) + const accept = selectOutputType(cid, acceptHeader) this.log('output type %s', accept) diff --git a/packages/verified-fetch/test/verified-fetch.spec.ts b/packages/verified-fetch/test/verified-fetch.spec.ts index 64cbc84..bdd0af9 100644 --- a/packages/verified-fetch/test/verified-fetch.spec.ts +++ b/packages/verified-fetch/test/verified-fetch.spec.ts @@ -710,4 +710,62 @@ describe('@helia/verifed-fetch', () => { expect(output).to.deep.equal(obj) }) }) + + describe('?format', () => { + let helia: Helia + let verifiedFetch: VerifiedFetch + let contentTypeParser: Sinon.SinonStub + + beforeEach(async () => { + contentTypeParser = Sinon.stub() + helia = await createHelia() + verifiedFetch = new VerifiedFetch({ + helia + }, { + contentTypeParser + }) + }) + + afterEach(async () => { + await stop(helia, verifiedFetch) + }) + + it('cbor?format=dag-json should be able to override curl/browser default accept header when query parameter is provided', async () => { + const obj = { + hello: 'world' + } + const c = dagCbor(helia) + const cid = await c.add(obj) + + const resp = await verifiedFetch.fetch(`http://example.com/ipfs/${cid}?format=dag-json`, { + headers: { + // see https://github.com/ipfs/helia-verified-fetch/issues/35 + // accept: '*/*' + } + }) + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipld.dag-json') + const data = ipldDagJson.decode(await resp.arrayBuffer()) + expect(data).to.deep.equal(obj) + }) + + it('raw?format=dag-json should be able to override curl/browser default accept header when query parameter is provided', async () => { + const finalRootFileContent = uint8ArrayFromString(JSON.stringify({ + hello: 'world' + })) + const cid = CID.createV1(raw.code, await sha256.digest(finalRootFileContent)) + await helia.blockstore.put(cid, finalRootFileContent) + + const resp = await verifiedFetch.fetch(`http://example.com/ipfs/${cid}?format=dag-json`, { + headers: { + // see https://github.com/ipfs/helia-verified-fetch/issues/35 + accept: '*/*' + } + }) + expect(resp).to.be.ok() + expect(resp.status).to.equal(200) + expect(resp.statusText).to.equal('OK') + const data = await resp.arrayBuffer() + expect(new Uint8Array(data)).to.equalBytes(finalRootFileContent) + }) + }) }) From b013774406478e85f2025c6090f91ca2b103d005 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:03:54 -0700 Subject: [PATCH 2/6] chore: some cleanup and optimizations --- packages/verified-fetch/src/verified-fetch.ts | 38 +++++-------------- .../test/verified-fetch.spec.ts | 1 + 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index b9edf8a..56ebb18 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -20,12 +20,13 @@ import { ByteRangeContext } from './utils/byte-range-context.js' import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js' import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js' import { getETag } from './utils/get-e-tag.js' +import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js' import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js' import { tarStream } from './utils/get-tar-stream.js' import { parseResource } from './utils/parse-resource.js' import { setCacheControlHeader } from './utils/response-headers.js' import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js' -import { selectOutputType, queryFormatToAcceptHeader, FORMAT_TO_MIME_TYPE } from './utils/select-output-type.js' +import { selectOutputType } from './utils/select-output-type.js' import { walkPath } from './utils/walk-path.js' import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js' import type { RequestFormatShorthand } from './types.js' @@ -93,6 +94,7 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit { + private async handleRaw ({ resource, cid, path, options, accept }: FetchHandlerFunctionArg): Promise { const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers) const result = await this.helia.blockstore.get(cid, options) byteRangeContext.setBody(result) @@ -396,7 +399,7 @@ export class VerifiedFetch { // if the user has specified an `Accept` header that corresponds to a raw // type, honour that header, so for example they don't request // `application/vnd.ipld.raw` but get `application/octet-stream` - const overriddenContentType = getOverridenRawContentType(options?.headers) + const overriddenContentType = getOverridenRawContentType({ headers: options?.headers, accept }) if (overriddenContentType != null) { response.headers.set('content-type', overriddenContentType) } else { @@ -499,28 +502,7 @@ export class VerifiedFetch { options?.onProgress?.(new CustomProgressEvent('verified-fetch:request:resolve', { cid, path })) - const requestHeaders = new Headers(options?.headers) - const incomingAcceptHeader = requestHeaders.get('accept') - - if (incomingAcceptHeader != null) { - this.log('incoming accept header "%s"', incomingAcceptHeader) - } - - const queryFormatMapping = queryFormatToAcceptHeader(query.format) - - if (query.format != null) { - this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping) - } - - let acceptHeader: string | undefined - // if the incomingAcceptHeader is autogenerated by the requesting client (browser/curl/fetch/etc) then we may need to override it if query.format is specified - if (queryFormatMapping != null && (incomingAcceptHeader == null || !Object.values(FORMAT_TO_MIME_TYPE).includes(incomingAcceptHeader))) { - this.log('accept header not recognized, but query format provided, setting accept header to %s', queryFormatMapping) - acceptHeader = queryFormatMapping - } else { - acceptHeader = incomingAcceptHeader ?? queryFormatMapping - } - this.log('determined accept header "%s"', acceptHeader) + const acceptHeader = getResolvedAcceptHeader({ query, headers: options?.headers, logger: this.helia.logger }) const accept = selectOutputType(cid, acceptHeader) this.log('output type %s', accept) @@ -532,7 +514,7 @@ export class VerifiedFetch { let response: Response let reqFormat: RequestFormatShorthand | undefined - const handlerArgs = { resource: resource.toString(), cid, path, accept, options } + const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, options } if (accept === 'application/vnd.ipfs.ipns-record') { // the user requested a raw IPNS record diff --git a/packages/verified-fetch/test/verified-fetch.spec.ts b/packages/verified-fetch/test/verified-fetch.spec.ts index bdd0af9..59bb027 100644 --- a/packages/verified-fetch/test/verified-fetch.spec.ts +++ b/packages/verified-fetch/test/verified-fetch.spec.ts @@ -765,6 +765,7 @@ describe('@helia/verifed-fetch', () => { expect(resp.status).to.equal(200) expect(resp.statusText).to.equal('OK') const data = await resp.arrayBuffer() + expect(resp.headers.get('content-type')).to.equal('application/vnd.ipld.dag-json') expect(new Uint8Array(data)).to.equalBytes(finalRootFileContent) }) }) From 54b7454baa8e9e7d4ac93cdf53b06ad08c7be51a Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:15:30 -0700 Subject: [PATCH 3/6] chore: forgot to add files --- .../src/utils/get-resolved-accept-header.ts | 42 +++++++++++++++++++ .../src/utils/is-accept-explicit.ts | 32 ++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/verified-fetch/src/utils/get-resolved-accept-header.ts create mode 100644 packages/verified-fetch/src/utils/is-accept-explicit.ts diff --git a/packages/verified-fetch/src/utils/get-resolved-accept-header.ts b/packages/verified-fetch/src/utils/get-resolved-accept-header.ts new file mode 100644 index 0000000..05d4d5d --- /dev/null +++ b/packages/verified-fetch/src/utils/get-resolved-accept-header.ts @@ -0,0 +1,42 @@ +import { isExplicitAcceptHeader, isExplicitFormatQuery, isExplicitIpldAcceptRequest } from './is-accept-explicit.js' +import { queryFormatToAcceptHeader } from './select-output-type.js' +import type { ParsedUrlStringResults } from './parse-url-string.js' +import type { ComponentLogger } from '@libp2p/interface' + +export interface ResolvedAcceptHeaderOptions { + query?: ParsedUrlStringResults['query'] + headers?: RequestInit['headers'] + logger?: ComponentLogger +} + +export function getResolvedAcceptHeader ({ query, headers, logger }: ResolvedAcceptHeaderOptions): string | undefined { + const log = logger?.forComponent('helia:verified-fetch:get-resolved-accept-header') + const requestHeaders = new Headers(headers) + const incomingAcceptHeader = requestHeaders.get('accept') ?? undefined + + if (incomingAcceptHeader != null) { + log?.('incoming accept header "%s"', incomingAcceptHeader) + } + + if (!isExplicitIpldAcceptRequest({ query, headers: requestHeaders })) { + log?.('no explicit IPLD content-type requested, returning incoming accept header %s', incomingAcceptHeader) + return incomingAcceptHeader + } + + const queryFormatMapping = queryFormatToAcceptHeader(query?.format) + + if (query?.format != null) { + log?.('incoming query format "%s", mapped to %s', query.format, queryFormatMapping) + } + + let acceptHeader = incomingAcceptHeader + // if the incomingAcceptHeader is autogenerated by the requesting client (browser/curl/fetch/etc) then we may need to override it if query.format is specified + if (!isExplicitAcceptHeader(requestHeaders) && isExplicitFormatQuery(query)) { + log?.('accept header not recognized, but query format provided, setting accept header to %s', queryFormatMapping) + acceptHeader = queryFormatMapping + } + + log?.('resolved accept header to "%s"', acceptHeader) + + return acceptHeader +} diff --git a/packages/verified-fetch/src/utils/is-accept-explicit.ts b/packages/verified-fetch/src/utils/is-accept-explicit.ts new file mode 100644 index 0000000..934b334 --- /dev/null +++ b/packages/verified-fetch/src/utils/is-accept-explicit.ts @@ -0,0 +1,32 @@ +import { FORMAT_TO_MIME_TYPE } from './select-output-type.js' +import type { ParsedUrlStringResults } from './parse-url-string.js' + +export interface IsAcceptExplicitOptions { + + query?: ParsedUrlStringResults['query'] + headers: Headers +} + +export function isExplicitAcceptHeader (headers: Headers): boolean { + const incomingAcceptHeader = headers.get('accept') + if (incomingAcceptHeader != null && Object.values(FORMAT_TO_MIME_TYPE).includes(incomingAcceptHeader)) { + return true + } + return false +} + +export function isExplicitFormatQuery (query?: ParsedUrlStringResults['query']): boolean { + const formatQuery = query?.format + if (formatQuery != null && Object.keys(FORMAT_TO_MIME_TYPE).includes(formatQuery)) { + return true + } + return false +} + +/** + * The user can provide an explicit `accept` header in the request headers or a `format` query parameter in the URL. + * If either of these are provided, this function returns true. + */ +export function isExplicitIpldAcceptRequest ({ query, headers }: IsAcceptExplicitOptions): boolean { + return isExplicitAcceptHeader(headers) || isExplicitFormatQuery(query) +} From 7acf56a5f3b8640771d63467143e4dd541375f56 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:46:21 -0700 Subject: [PATCH 4/6] fix: walking dag-cbor paths --- .../verified-fetch/src/utils/responses.ts | 13 ++++++ .../verified-fetch/src/utils/walk-path.ts | 9 +++- packages/verified-fetch/src/verified-fetch.ts | 41 +++++++++++++++++-- .../test/verified-fetch.spec.ts | 31 ++++++++++++++ 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/verified-fetch/src/utils/responses.ts b/packages/verified-fetch/src/utils/responses.ts index dda0230..1c2d85e 100644 --- a/packages/verified-fetch/src/utils/responses.ts +++ b/packages/verified-fetch/src/utils/responses.ts @@ -85,6 +85,19 @@ export function notAcceptableResponse (url: string, body?: SupportedBodyTypes, i return response } +export function notFoundResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response { + const response = new Response(body, { + ...(init ?? {}), + status: 404, + statusText: 'Not Found' + }) + + setType(response, 'basic') + setUrl(response, url) + + return response +} + /** * if body is an Error, it will be converted to a string containing the error message. */ diff --git a/packages/verified-fetch/src/utils/walk-path.ts b/packages/verified-fetch/src/utils/walk-path.ts index 45f2066..1d14e3e 100644 --- a/packages/verified-fetch/src/utils/walk-path.ts +++ b/packages/verified-fetch/src/utils/walk-path.ts @@ -1,4 +1,5 @@ -import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type UnixFSEntry } from 'ipfs-unixfs-exporter' +import { CodeError } from '@libp2p/interface' +import { walkPath as exporterWalk, type ExporterOptions, type ReadableStorage, type ObjectNode, type UnixFSEntry } from 'ipfs-unixfs-exporter' import type { CID } from 'multiformats/cid' export interface PathWalkerOptions extends ExporterOptions { @@ -24,7 +25,7 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio } if (terminalElement == null) { - throw new Error('No terminal element found') + throw new CodeError('No terminal element found', 'NO_TERMINAL_ELEMENT') } return { @@ -32,3 +33,7 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio terminalElement } } + +export function objectNodeGuard (node: UnixFSEntry): node is ObjectNode { + return node.type === 'object' +} diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 56ebb18..5e6a75e 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -25,15 +25,15 @@ import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterab import { tarStream } from './utils/get-tar-stream.js' import { parseResource } from './utils/parse-resource.js' import { setCacheControlHeader } from './utils/response-headers.js' -import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js' +import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js' import { selectOutputType } from './utils/select-output-type.js' -import { walkPath } from './utils/walk-path.js' +import { objectNodeGuard, walkPath } from './utils/walk-path.js' import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js' import type { RequestFormatShorthand } from './types.js' import type { ParsedUrlStringResults } from './utils/parse-url-string' import type { Helia } from '@helia/interface' import type { DNSResolver } from '@multiformats/dns/resolvers' -import type { UnixFSEntry } from 'ipfs-unixfs-exporter' +import type { ObjectNode, UnixFSEntry } from 'ipfs-unixfs-exporter' import type { CID } from 'multiformats/cid' interface VerifiedFetchComponents { @@ -236,8 +236,33 @@ export class VerifiedFetch { private async handleDagCbor ({ resource, cid, path, accept, options }: FetchHandlerFunctionArg): Promise { this.log.trace('fetching %c/%s', cid, path) + let terminalElement: ObjectNode | undefined + let ipfsRoots: CID[] | undefined + + // need to walk path, if it exists, to get the terminal element + try { + const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options) + ipfsRoots = pathDetails.ipfsRoots + const potentialTerminalElement = pathDetails.terminalElement + if (potentialTerminalElement == null) { + return notFoundResponse(resource.toString()) + } + if (objectNodeGuard(potentialTerminalElement)) { + terminalElement = potentialTerminalElement + } + } catch (err: any) { + if (options?.signal?.aborted === true) { + throw new AbortError('signal aborted by user') + } + if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) { + return notFoundResponse(resource.toString()) + } + + this.log.error('error walking path %s', path, err) + return badGatewayResponse(resource.toString(), 'Error walking path') + } + const block = terminalElement?.node ?? await this.helia.blockstore.get(cid, options) - const block = await this.helia.blockstore.get(cid, options) let body: string | Uint8Array if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') { @@ -277,9 +302,14 @@ export class VerifiedFetch { response.headers.set('content-type', accept) + if (ipfsRoots != null) { + response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header + } + return response } + // eslint-disable-next-line complexity private async handleDagPb ({ cid, path, resource, options }: FetchHandlerFunctionArg): Promise { let terminalElement: UnixFSEntry | undefined let ipfsRoots: CID[] | undefined @@ -294,6 +324,9 @@ export class VerifiedFetch { if (options?.signal?.aborted === true) { throw new AbortError('signal aborted by user') } + if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) { + return notFoundResponse(resource.toString()) + } this.log.error('error walking path %s', path, err) return badGatewayResponse(resource.toString(), 'Error walking path') diff --git a/packages/verified-fetch/test/verified-fetch.spec.ts b/packages/verified-fetch/test/verified-fetch.spec.ts index 59bb027..39d3a03 100644 --- a/packages/verified-fetch/test/verified-fetch.spec.ts +++ b/packages/verified-fetch/test/verified-fetch.spec.ts @@ -769,4 +769,35 @@ describe('@helia/verifed-fetch', () => { expect(new Uint8Array(data)).to.equalBytes(finalRootFileContent) }) }) + + describe('404 paths', () => { + let helia: Helia + let verifiedFetch: VerifiedFetch + let contentTypeParser: Sinon.SinonStub + + beforeEach(async () => { + contentTypeParser = Sinon.stub() + helia = await createHelia() + verifiedFetch = new VerifiedFetch({ + helia + }, { + contentTypeParser + }) + }) + + afterEach(async () => { + await stop(helia, verifiedFetch) + }) + + it('returns a 404 when walking dag-cbor for non-existent path', async () => { + const obj = { + hello: 'world' + } + const c = dagCbor(helia) + const cid = await c.add(obj) + + const resp = await verifiedFetch.fetch(`http://example.com/ipfs/${cid}/foo/i-do-not-exist`) + expect(resp.status).to.equal(404) + }) + }) }) From 75976134d6a27299a16c38d2b5e8d95752e8c2ed Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:16:43 -0700 Subject: [PATCH 5/6] chore: suggestions from code review Co-authored-by: Alex Potsides --- packages/verified-fetch/src/utils/walk-path.ts | 4 ++-- packages/verified-fetch/src/verified-fetch.ts | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/verified-fetch/src/utils/walk-path.ts b/packages/verified-fetch/src/utils/walk-path.ts index 1d14e3e..be46fdf 100644 --- a/packages/verified-fetch/src/utils/walk-path.ts +++ b/packages/verified-fetch/src/utils/walk-path.ts @@ -25,7 +25,7 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio } if (terminalElement == null) { - throw new CodeError('No terminal element found', 'NO_TERMINAL_ELEMENT') + throw new CodeError('No terminal element found', 'ERR_NO_TERMINAL_ELEMENT') } return { @@ -34,6 +34,6 @@ export async function walkPath (blockstore: ReadableStorage, path: string, optio } } -export function objectNodeGuard (node: UnixFSEntry): node is ObjectNode { +export function isObjectNode (node: UnixFSEntry): node is ObjectNode { return node.type === 'object' } diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 5e6a75e..6682b99 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -27,7 +27,7 @@ import { parseResource } from './utils/parse-resource.js' import { setCacheControlHeader } from './utils/response-headers.js' import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js' import { selectOutputType } from './utils/select-output-type.js' -import { objectNodeGuard, walkPath } from './utils/walk-path.js' +import { isObjectNode, walkPath } from './utils/walk-path.js' import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js' import type { RequestFormatShorthand } from './types.js' import type { ParsedUrlStringResults } from './utils/parse-url-string' @@ -245,21 +245,19 @@ export class VerifiedFetch { ipfsRoots = pathDetails.ipfsRoots const potentialTerminalElement = pathDetails.terminalElement if (potentialTerminalElement == null) { - return notFoundResponse(resource.toString()) + return notFoundResponse(resource) } - if (objectNodeGuard(potentialTerminalElement)) { + if (isObjectNode(potentialTerminalElement)) { terminalElement = potentialTerminalElement } } catch (err: any) { - if (options?.signal?.aborted === true) { - throw new AbortError('signal aborted by user') - } - if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) { - return notFoundResponse(resource.toString()) + options?.signal?.throwIfAborted() + if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) { + return notFoundResponse(resource) } this.log.error('error walking path %s', path, err) - return badGatewayResponse(resource.toString(), 'Error walking path') + return badGatewayResponse(resource, 'Error walking path') } const block = terminalElement?.node ?? await this.helia.blockstore.get(cid, options) @@ -324,7 +322,7 @@ export class VerifiedFetch { if (options?.signal?.aborted === true) { throw new AbortError('signal aborted by user') } - if (['ERR_NO_PROP', 'NO_TERMINAL_ELEMENT'].includes(err.code)) { + if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) { return notFoundResponse(resource.toString()) } this.log.error('error walking path %s', path, err) From 67b14cf63bcaea9f8a5703434069ae6ceaf46034 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:28:23 -0700 Subject: [PATCH 6/6] chore: use signal.throwIfAborted --- packages/verified-fetch/src/verified-fetch.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/verified-fetch/src/verified-fetch.ts b/packages/verified-fetch/src/verified-fetch.ts index 6682b99..31e0fed 100644 --- a/packages/verified-fetch/src/verified-fetch.ts +++ b/packages/verified-fetch/src/verified-fetch.ts @@ -4,7 +4,7 @@ import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs } from '@helia/unixfs import * as ipldDagCbor from '@ipld/dag-cbor' import * as ipldDagJson from '@ipld/dag-json' import { code as dagPbCode } from '@ipld/dag-pb' -import { AbortError, type AbortOptions, type Logger, type PeerId } from '@libp2p/interface' +import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface' import { Record as DHTRecord } from '@libp2p/kad-dht' import { peerIdFromString } from '@libp2p/peer-id' import { Key } from 'interface-datastore' @@ -307,7 +307,6 @@ export class VerifiedFetch { return response } - // eslint-disable-next-line complexity private async handleDagPb ({ cid, path, resource, options }: FetchHandlerFunctionArg): Promise { let terminalElement: UnixFSEntry | undefined let ipfsRoots: CID[] | undefined @@ -319,9 +318,7 @@ export class VerifiedFetch { ipfsRoots = pathDetails.ipfsRoots terminalElement = pathDetails.terminalElement } catch (err: any) { - if (options?.signal?.aborted === true) { - throw new AbortError('signal aborted by user') - } + options?.signal?.throwIfAborted() if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT'].includes(err.code)) { return notFoundResponse(resource.toString()) } @@ -362,9 +359,7 @@ export class VerifiedFetch { path = rootFilePath resolvedCID = stat.cid } catch (err: any) { - if (options?.signal?.aborted === true) { - throw new AbortError('signal aborted by user') - } + options?.signal?.throwIfAborted() this.log('error loading path %c/%s', dirCid, rootFilePath, err) return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented') } finally { @@ -408,9 +403,7 @@ export class VerifiedFetch { return response } catch (err: any) { - if (options?.signal?.aborted === true) { - throw new AbortError('signal aborted by user') - } + options?.signal?.throwIfAborted() this.log.error('error streaming %c/%s', cid, path, err) if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') { return badRangeResponse(resource) @@ -495,7 +488,6 @@ export class VerifiedFetch { * TODO: move operations called by fetch to a queue of operations where we can * always exit early (and cleanly) if a given signal is aborted */ - // eslint-disable-next-line complexity async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise { this.log('fetch %s', resource) @@ -523,9 +515,7 @@ export class VerifiedFetch { ttl = result.ttl protocol = result.protocol } catch (err: any) { - if (options?.signal?.aborted === true) { - throw new AbortError('signal aborted by user') - } + options?.signal?.throwIfAborted() this.log.error('error parsing resource %s', resource, err) return badRequestResponse(resource.toString(), err)