@helia/verified-fetch 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/index.min.js +5 -5
  2. package/dist/src/utils/get-stream-from-async-iterable.d.ts +2 -2
  3. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
  4. package/dist/src/utils/get-stream-from-async-iterable.js +6 -0
  5. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
  6. package/dist/src/utils/parse-resource.d.ts +3 -4
  7. package/dist/src/utils/parse-resource.d.ts.map +1 -1
  8. package/dist/src/utils/parse-resource.js +3 -2
  9. package/dist/src/utils/parse-resource.js.map +1 -1
  10. package/dist/src/utils/parse-url-string.d.ts +13 -8
  11. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  12. package/dist/src/utils/parse-url-string.js +62 -10
  13. package/dist/src/utils/parse-url-string.js.map +1 -1
  14. package/dist/src/utils/response-headers.d.ts +19 -0
  15. package/dist/src/utils/response-headers.d.ts.map +1 -1
  16. package/dist/src/utils/response-headers.js +25 -0
  17. package/dist/src/utils/response-headers.js.map +1 -1
  18. package/dist/src/utils/responses.d.ts +4 -1
  19. package/dist/src/utils/responses.d.ts.map +1 -1
  20. package/dist/src/utils/responses.js +6 -0
  21. package/dist/src/utils/responses.js.map +1 -1
  22. package/dist/src/verified-fetch.d.ts +12 -0
  23. package/dist/src/verified-fetch.d.ts.map +1 -1
  24. package/dist/src/verified-fetch.js +49 -6
  25. package/dist/src/verified-fetch.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/utils/get-stream-from-async-iterable.ts +7 -2
  28. package/src/utils/parse-resource.ts +7 -7
  29. package/src/utils/parse-url-string.ts +88 -21
  30. package/src/utils/response-headers.ts +36 -0
  31. package/src/utils/responses.ts +7 -1
  32. package/src/verified-fetch.ts +54 -9
@@ -4,6 +4,7 @@ import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs } from '@helia/unixfs
4
4
  import * as ipldDagCbor from '@ipld/dag-cbor'
5
5
  import * as ipldDagJson from '@ipld/dag-json'
6
6
  import { code as dagPbCode } from '@ipld/dag-pb'
7
+ import { AbortError, type AbortOptions, type Logger, type PeerId } from '@libp2p/interface'
7
8
  import { Record as DHTRecord } from '@libp2p/kad-dht'
8
9
  import { peerIdFromString } from '@libp2p/peer-id'
9
10
  import { Key } from 'interface-datastore'
@@ -22,6 +23,7 @@ import { getETag } from './utils/get-e-tag.js'
22
23
  import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
23
24
  import { tarStream } from './utils/get-tar-stream.js'
24
25
  import { parseResource } from './utils/parse-resource.js'
26
+ import { setCacheControlHeader } from './utils/response-headers.js'
25
27
  import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse } from './utils/responses.js'
26
28
  import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
27
29
  import { walkPath } from './utils/walk-path.js'
@@ -29,7 +31,6 @@ import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as Verif
29
31
  import type { RequestFormatShorthand } from './types.js'
30
32
  import type { ParsedUrlStringResults } from './utils/parse-url-string'
31
33
  import type { Helia } from '@helia/interface'
32
- import type { AbortOptions, Logger, PeerId } from '@libp2p/interface'
33
34
  import type { DNSResolver } from '@multiformats/dns/resolvers'
34
35
  import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
35
36
  import type { CID } from 'multiformats/cid'
@@ -77,7 +78,10 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
77
78
  let signal: AbortSignal | undefined
78
79
  if (options?.signal === null) {
79
80
  signal = undefined
81
+ } else {
82
+ signal = options?.signal
80
83
  }
84
+
81
85
  return {
82
86
  ...options,
83
87
  signal
@@ -283,10 +287,13 @@ export class VerifiedFetch {
283
287
  const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
284
288
  ipfsRoots = pathDetails.ipfsRoots
285
289
  terminalElement = pathDetails.terminalElement
286
- } catch (err) {
290
+ } catch (err: any) {
291
+ if (options?.signal?.aborted === true) {
292
+ throw new AbortError('signal aborted by user')
293
+ }
287
294
  this.log.error('error walking path %s', path, err)
288
295
 
289
- return badGatewayResponse('Error walking path')
296
+ return badGatewayResponse(resource.toString(), 'Error walking path')
290
297
  }
291
298
 
292
299
  let resolvedCID = terminalElement?.cid ?? cid
@@ -320,6 +327,9 @@ export class VerifiedFetch {
320
327
  path = rootFilePath
321
328
  resolvedCID = stat.cid
322
329
  } catch (err: any) {
330
+ if (options?.signal?.aborted === true) {
331
+ throw new AbortError('signal aborted by user')
332
+ }
323
333
  this.log('error loading path %c/%s', dirCid, rootFilePath, err)
324
334
  return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
325
335
  } finally {
@@ -346,7 +356,8 @@ export class VerifiedFetch {
346
356
 
347
357
  try {
348
358
  const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
349
- onProgress: options?.onProgress
359
+ onProgress: options?.onProgress,
360
+ signal: options?.signal
350
361
  })
351
362
  byteRangeContext.setBody(stream)
352
363
  // if not a valid range request, okRangeRequest will call okResponse
@@ -362,11 +373,14 @@ export class VerifiedFetch {
362
373
 
363
374
  return response
364
375
  } catch (err: any) {
376
+ if (options?.signal?.aborted === true) {
377
+ throw new AbortError('signal aborted by user')
378
+ }
365
379
  this.log.error('error streaming %c/%s', cid, path, err)
366
380
  if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
367
381
  return badRangeResponse(resource)
368
382
  }
369
- return badGatewayResponse('Unable to stream content')
383
+ return badGatewayResponse(resource.toString(), 'Unable to stream content')
370
384
  }
371
385
  }
372
386
 
@@ -430,26 +444,56 @@ export class VerifiedFetch {
430
444
  [identity.code]: this.handleRaw
431
445
  }
432
446
 
447
+ /**
448
+ *
449
+ * TODO: Should we use 400, 408, 418, or 425, or throw and not even return a response?
450
+ */
451
+ private async abortHandler (opController: AbortController): Promise<void> {
452
+ this.log.error('signal aborted by user')
453
+ opController.abort('signal aborted by user')
454
+ }
455
+
456
+ /**
457
+ * We're starting to get to the point where we need a queue or pipeline of
458
+ * operations to perform and a single place to handle errors.
459
+ *
460
+ * TODO: move operations called by fetch to a queue of operations where we can
461
+ * always exit early (and cleanly) if a given signal is aborted
462
+ */
463
+ // eslint-disable-next-line complexity
433
464
  async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
434
465
  this.log('fetch %s', resource)
435
466
 
436
467
  const options = convertOptions(opts)
437
468
 
469
+ const opController = new AbortController()
470
+ if (options?.signal != null) {
471
+ options.signal.onabort = this.abortHandler.bind(this, opController)
472
+ options.signal = opController.signal
473
+ }
474
+
438
475
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
439
476
 
440
477
  // resolve the CID/path from the requested resource
441
478
  let cid: ParsedUrlStringResults['cid']
442
479
  let path: ParsedUrlStringResults['path']
443
480
  let query: ParsedUrlStringResults['query']
481
+ let ttl: ParsedUrlStringResults['ttl']
482
+ let protocol: ParsedUrlStringResults['protocol']
444
483
  try {
445
484
  const result = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
446
485
  cid = result.cid
447
486
  path = result.path
448
487
  query = result.query
449
- } catch (err) {
488
+ ttl = result.ttl
489
+ protocol = result.protocol
490
+ } catch (err: any) {
491
+ if (options?.signal?.aborted === true) {
492
+ throw new AbortError('signal aborted by user')
493
+ }
450
494
  this.log.error('error parsing resource %s', resource, err)
451
495
 
452
- return badRequestResponse('Invalid resource')
496
+ return badRequestResponse(resource.toString(), err)
453
497
  }
454
498
 
455
499
  options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
@@ -508,7 +552,7 @@ export class VerifiedFetch {
508
552
  const codecHandler = this.codecHandlers[cid.code]
509
553
 
510
554
  if (codecHandler == null) {
511
- return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
555
+ return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia-verified-fetch/issues/new`)
512
556
  }
513
557
  this.log.trace('calling handler "%s"', codecHandler.name)
514
558
 
@@ -516,7 +560,8 @@ export class VerifiedFetch {
516
560
  }
517
561
 
518
562
  response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
519
- response.headers.set('cache-control', 'public, max-age=29030400, immutable')
563
+
564
+ setCacheControlHeader({ response, ttl, protocol })
520
565
  // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
521
566
  response.headers.set('X-Ipfs-Path', resource.toString())
522
567