@helia/verified-fetch 2.6.4 → 2.6.6

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 (68) hide show
  1. package/dist/index.min.js +109 -44
  2. package/dist/src/plugins/plugin-handle-byte-range-context.d.ts +13 -0
  3. package/dist/src/plugins/plugin-handle-byte-range-context.d.ts.map +1 -0
  4. package/dist/src/plugins/plugin-handle-byte-range-context.js +19 -0
  5. package/dist/src/plugins/plugin-handle-byte-range-context.js.map +1 -0
  6. package/dist/src/plugins/plugin-handle-car.d.ts +1 -1
  7. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  8. package/dist/src/plugins/plugin-handle-car.js +9 -2
  9. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  10. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts +2 -2
  11. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
  12. package/dist/src/plugins/plugin-handle-dag-cbor.js +7 -6
  13. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  14. package/dist/src/plugins/plugin-handle-dag-pb.d.ts +2 -2
  15. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
  16. package/dist/src/plugins/plugin-handle-dag-pb.js +5 -6
  17. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  18. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts +1 -1
  19. package/dist/src/plugins/plugin-handle-dir-index-html.d.ts.map +1 -1
  20. package/dist/src/plugins/plugin-handle-dir-index-html.js +7 -15
  21. package/dist/src/plugins/plugin-handle-dir-index-html.js.map +1 -1
  22. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +2 -2
  23. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  24. package/dist/src/plugins/plugin-handle-ipns-record.js +8 -4
  25. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  26. package/dist/src/plugins/plugin-handle-json.d.ts +2 -2
  27. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  28. package/dist/src/plugins/plugin-handle-json.js +7 -3
  29. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  30. package/dist/src/plugins/plugin-handle-raw.d.ts +2 -2
  31. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  32. package/dist/src/plugins/plugin-handle-raw.js +6 -5
  33. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  34. package/dist/src/plugins/plugin-handle-tar.d.ts +2 -2
  35. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  36. package/dist/src/plugins/plugin-handle-tar.js +7 -3
  37. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  38. package/dist/src/plugins/types.d.ts +10 -1
  39. package/dist/src/plugins/types.d.ts.map +1 -1
  40. package/dist/src/utils/byte-range-context.d.ts.map +1 -1
  41. package/dist/src/utils/byte-range-context.js +1 -0
  42. package/dist/src/utils/byte-range-context.js.map +1 -1
  43. package/dist/src/utils/parse-url-string.d.ts +4 -2
  44. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  45. package/dist/src/utils/parse-url-string.js +15 -7
  46. package/dist/src/utils/parse-url-string.js.map +1 -1
  47. package/dist/src/utils/responses.d.ts +1 -4
  48. package/dist/src/utils/responses.d.ts.map +1 -1
  49. package/dist/src/utils/responses.js +25 -9
  50. package/dist/src/utils/responses.js.map +1 -1
  51. package/dist/src/verified-fetch.d.ts.map +1 -1
  52. package/dist/src/verified-fetch.js +13 -1
  53. package/dist/src/verified-fetch.js.map +1 -1
  54. package/package.json +19 -19
  55. package/src/plugins/plugin-handle-byte-range-context.ts +22 -0
  56. package/src/plugins/plugin-handle-car.ts +10 -3
  57. package/src/plugins/plugin-handle-dag-cbor.ts +10 -7
  58. package/src/plugins/plugin-handle-dag-pb.ts +6 -7
  59. package/src/plugins/plugin-handle-dir-index-html.ts +11 -16
  60. package/src/plugins/plugin-handle-ipns-record.ts +10 -5
  61. package/src/plugins/plugin-handle-json.ts +9 -4
  62. package/src/plugins/plugin-handle-raw.ts +7 -6
  63. package/src/plugins/plugin-handle-tar.ts +9 -4
  64. package/src/plugins/types.ts +10 -1
  65. package/src/utils/byte-range-context.ts +1 -0
  66. package/src/utils/parse-url-string.ts +16 -7
  67. package/src/utils/responses.ts +27 -9
  68. package/src/verified-fetch.ts +14 -1
@@ -2,7 +2,7 @@ import * as ipldDagCbor from '@ipld/dag-cbor'
2
2
  import * as ipldDagJson from '@ipld/dag-json'
3
3
  import { dagCborToSafeJSON } from '../utils/dag-cbor-to-safe-json.js'
4
4
  import { setIpfsRoots } from '../utils/response-headers.js'
5
- import { notAcceptableResponse, okResponse } from '../utils/responses.js'
5
+ import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
6
6
  import { isObjectNode } from '../utils/walk-path.js'
7
7
  import { BasePlugin } from './plugin-base.js'
8
8
  import type { PluginContext } from './types.js'
@@ -14,7 +14,7 @@ import type { ObjectNode } from 'ipfs-unixfs-exporter'
14
14
  export class DagCborPlugin extends BasePlugin {
15
15
  readonly codes = [ipldDagCbor.code]
16
16
 
17
- canHandle ({ cid, accept, pathDetails }: PluginContext): boolean {
17
+ canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
18
18
  this.log('checking if we can handle %c with accept %s', cid, accept)
19
19
  if (pathDetails == null) {
20
20
  return false
@@ -25,17 +25,18 @@ export class DagCborPlugin extends BasePlugin {
25
25
  if (cid.code !== ipldDagCbor.code) {
26
26
  return false
27
27
  }
28
+ if (byteRangeContext == null) {
29
+ return false
30
+ }
28
31
 
29
32
  return isObjectNode(pathDetails.terminalElement)
30
33
  }
31
34
 
32
- async handle (context: PluginContext): Promise<Response> {
35
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response> {
33
36
  const { cid, path, resource, accept, pathDetails } = context
34
37
 
35
38
  this.log.trace('fetching %c/%s', cid, path)
36
- if (pathDetails == null) {
37
- throw new Error('pathDetails is null')
38
- }
39
+
39
40
  const ipfsRoots = pathDetails.ipfsRoots
40
41
  const terminalElement = pathDetails.terminalElement as ObjectNode // checked in canHandle fn.
41
42
 
@@ -72,7 +73,9 @@ export class DagCborPlugin extends BasePlugin {
72
73
  }
73
74
  }
74
75
 
75
- const response = okResponse(resource, body)
76
+ context.byteRangeContext.setBody(body)
77
+
78
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
76
79
 
77
80
  const responseContentType = accept ?? (body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
78
81
 
@@ -2,7 +2,6 @@ import { unixfs } from '@helia/unixfs'
2
2
  import { code as dagPbCode } from '@ipld/dag-pb'
3
3
  import { exporter } from 'ipfs-unixfs-exporter'
4
4
  import { CustomProgressEvent } from 'progress-events'
5
- import { ByteRangeContext } from '../utils/byte-range-context.js'
6
5
  import { getStreamFromAsyncIterable } from '../utils/get-stream-from-async-iterable.js'
7
6
  import { setIpfsRoots } from '../utils/response-headers.js'
8
7
  import { badGatewayResponse, badRangeResponse, movedPermanentlyResponse, notSupportedResponse, okRangeResponse } from '../utils/responses.js'
@@ -16,11 +15,14 @@ import type { CIDDetail } from '../index.js'
16
15
  */
17
16
  export class DagPbPlugin extends BasePlugin {
18
17
  readonly codes = [dagPbCode]
19
- canHandle ({ cid, accept, pathDetails }: PluginContext): boolean {
18
+ canHandle ({ cid, accept, pathDetails, byteRangeContext }: PluginContext): boolean {
20
19
  this.log('checking if we can handle %c with accept %s', cid, accept)
21
20
  if (pathDetails == null) {
22
21
  return false
23
22
  }
23
+ if (byteRangeContext == null) {
24
+ return false
25
+ }
24
26
 
25
27
  return cid.code === dagPbCode
26
28
  }
@@ -49,7 +51,7 @@ export class DagPbPlugin extends BasePlugin {
49
51
  return null
50
52
  }
51
53
 
52
- async handle (context: PluginContext): Promise<Response | null> {
54
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails'>>): Promise<Response | null> {
53
55
  const { cid, options, withServerTiming = false, pathDetails } = context
54
56
  const { handleServerTiming, contentTypeParser, helia, getBlockstore } = this.pluginOptions
55
57
  const log = this.log
@@ -57,11 +59,8 @@ export class DagPbPlugin extends BasePlugin {
57
59
  let path = context.path
58
60
 
59
61
  let redirected = false
60
- const byteRangeContext = new ByteRangeContext(this.pluginOptions.logger, options?.headers)
61
62
 
62
- if (pathDetails == null) {
63
- throw new TypeError('Path details are required')
64
- }
63
+ const byteRangeContext = context.byteRangeContext
65
64
  const ipfsRoots = pathDetails.ipfsRoots
66
65
  const terminalElement = pathDetails.terminalElement
67
66
  let resolvedCID = terminalElement.cid
@@ -1,5 +1,6 @@
1
1
  import { code as dagPbCode } from '@ipld/dag-pb'
2
2
  import { dirIndexHtml } from '../utils/dir-index-html.js'
3
+ import { okRangeResponse } from '../utils/responses.js'
3
4
  import { BasePlugin } from './plugin-base.js'
4
5
  import type { PluginContext, VerifiedFetchPluginFactory } from './types.js'
5
6
 
@@ -21,29 +22,23 @@ export class DirIndexHtmlPlugin extends BasePlugin {
21
22
  return cid.code === dagPbCode
22
23
  }
23
24
 
24
- async handle (context: PluginContext): Promise<Response> {
25
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext' | 'pathDetails' | 'directoryEntries'>>): Promise<Response> {
25
26
  const { resource, pathDetails, directoryEntries } = context
26
27
 
27
- if (pathDetails?.terminalElement == null) {
28
- throw new Error('Path details are required')
29
- }
30
- if (directoryEntries == null || directoryEntries?.length === 0) {
31
- throw new Error('Directory entries are required')
32
- }
33
28
  const terminalElement = pathDetails.terminalElement
34
29
 
35
30
  const gatewayURL = resource
36
31
  const htmlResponse = dirIndexHtml(terminalElement, directoryEntries, { gatewayURL, log: this.log })
37
32
 
38
- return new Response(htmlResponse, {
39
- status: 200,
40
- statusText: 'OK',
41
- headers: {
42
- 'Content-Type': 'text/html',
43
- // see https://github.com/ipfs/gateway-conformance/pull/219
44
- 'Cache-Control': 'public, max-age=604800, stale-while-revalidate=2678400'
45
- }
46
- })
33
+ context.byteRangeContext.setBody(htmlResponse)
34
+
35
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
36
+
37
+ response.headers.set('Content-Type', 'text/html')
38
+ // see https://github.com/ipfs/gateway-conformance/pull/219
39
+ response.headers.set('Cache-Control', 'public, max-age=604800, stale-while-revalidate=2678400')
40
+
41
+ return response
47
42
  }
48
43
  }
49
44
 
@@ -4,7 +4,7 @@ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
4
4
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
5
5
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
6
6
  import { getPeerIdFromString } from '../utils/get-peer-id-from-string.js'
7
- import { badRequestResponse, okResponse } from '../utils/responses.js'
7
+ import { badRequestResponse, okRangeResponse } from '../utils/responses.js'
8
8
  import { PluginFatalError } from './errors.js'
9
9
  import { BasePlugin } from './plugin-base.js'
10
10
  import type { PluginContext } from './types.js'
@@ -16,19 +16,22 @@ import type { PeerId } from '@libp2p/interface'
16
16
  */
17
17
  export class IpnsRecordPlugin extends BasePlugin {
18
18
  readonly codes = []
19
- canHandle ({ cid, accept, query }: PluginContext): boolean {
19
+ canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
20
20
  this.log('checking if we can handle %c with accept %s', cid, accept)
21
+ if (byteRangeContext == null) {
22
+ return false
23
+ }
21
24
 
22
25
  return accept === 'application/vnd.ipfs.ipns-record' || query.format === 'ipns-record'
23
26
  }
24
27
 
25
- async handle (context: PluginContext): Promise<Response> {
28
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
26
29
  const { resource, path, options } = context
27
30
  const { helia } = this.pluginOptions
28
31
  context.reqFormat = 'ipns-record'
29
32
  if (path !== '' || !(resource.startsWith('ipns://') || resource.includes('.ipns.') || resource.includes('/ipns/'))) {
30
33
  this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
31
- throw new PluginFatalError('ERR_INVALID_IPNS_NAME', 'Invalid IPNS name', { response: badRequestResponse(resource, 'Invalid IPNS name') })
34
+ throw new PluginFatalError('ERR_INVALID_IPNS_NAME', 'Invalid IPNS name', { response: badRequestResponse(resource, new Error('Invalid IPNS name')) })
32
35
  }
33
36
  let peerId: PeerId
34
37
 
@@ -61,7 +64,9 @@ export class IpnsRecordPlugin extends BasePlugin {
61
64
  const buf = await helia.datastore.get(datastoreKey, options)
62
65
  const record = DHTRecord.deserialize(buf)
63
66
 
64
- const response = okResponse(resource, record.value)
67
+ context.byteRangeContext.setBody(record.value)
68
+
69
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
65
70
  response.headers.set('content-type', 'application/vnd.ipfs.ipns-record')
66
71
 
67
72
  return response
@@ -1,7 +1,7 @@
1
1
  import * as ipldDagCbor from '@ipld/dag-cbor'
2
2
  import * as ipldDagJson from '@ipld/dag-json'
3
3
  import { code as jsonCode } from 'multiformats/codecs/json'
4
- import { notAcceptableResponse, okResponse } from '../utils/responses.js'
4
+ import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
5
5
  import { BasePlugin } from './plugin-base.js'
6
6
  import type { PluginContext } from './types.js'
7
7
 
@@ -10,8 +10,11 @@ import type { PluginContext } from './types.js'
10
10
  */
11
11
  export class JsonPlugin extends BasePlugin {
12
12
  readonly codes = [ipldDagJson.code, jsonCode]
13
- canHandle ({ cid, accept }: PluginContext): boolean {
13
+ canHandle ({ cid, accept, byteRangeContext }: PluginContext): boolean {
14
14
  this.log('checking if we can handle %c with accept %s', cid, accept)
15
+ if (byteRangeContext == null) {
16
+ return false
17
+ }
15
18
 
16
19
  if (accept === 'application/vnd.ipld.dag-json' && cid.code !== ipldDagCbor.code) {
17
20
  // we can handle application/vnd.ipld.dag-json, but if the CID codec is ipldDagCbor, DagCborPlugin should handle it
@@ -22,7 +25,7 @@ export class JsonPlugin extends BasePlugin {
22
25
  return ipldDagJson.code === cid.code || jsonCode === cid.code
23
26
  }
24
27
 
25
- async handle (context: PluginContext): Promise<Response> {
28
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
26
29
  const { path, resource, cid, accept, options } = context
27
30
  const { getBlockstore } = this.pluginOptions
28
31
  const session = options?.session ?? true
@@ -50,7 +53,9 @@ export class JsonPlugin extends BasePlugin {
50
53
  body = block
51
54
  }
52
55
 
53
- const response = okResponse(resource, body)
56
+ context.byteRangeContext.setBody(body)
57
+
58
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
54
59
  response.headers.set('content-type', accept ?? 'application/json')
55
60
  return response
56
61
  }
@@ -1,6 +1,5 @@
1
1
  import { code as rawCode } from 'multiformats/codecs/raw'
2
2
  import { identity } from 'multiformats/hashes/identity'
3
- import { ByteRangeContext } from '../utils/byte-range-context.js'
4
3
  import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
5
4
  import { setContentType } from '../utils/set-content-type.js'
6
5
  import { PluginFatalError } from './errors.js'
@@ -46,12 +45,15 @@ function getOverridenRawContentType ({ headers, accept }: { headers?: HeadersIni
46
45
  export class RawPlugin extends BasePlugin {
47
46
  codes: number[] = [rawCode, identity.code]
48
47
 
49
- canHandle ({ cid, accept, query }: PluginContext): boolean {
48
+ canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
50
49
  this.log('checking if we can handle %c with accept %s', cid, accept)
50
+ if (byteRangeContext == null) {
51
+ return false
52
+ }
51
53
  return accept === 'application/vnd.ipld.raw' || query.format === 'raw'
52
54
  }
53
55
 
54
- async handle (context: PluginContext): Promise<Response> {
56
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
55
57
  const { path, resource, cid, accept, query, options } = context
56
58
  const { getBlockstore, contentTypeParser } = this.pluginOptions
57
59
  const session = options?.session ?? true
@@ -73,12 +75,11 @@ export class RawPlugin extends BasePlugin {
73
75
  throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
74
76
  }
75
77
 
76
- const byteRangeContext = new ByteRangeContext(this.pluginOptions.logger, options?.headers)
77
78
  const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
78
79
  const blockstore = getBlockstore(terminalCid, resource, session, options)
79
80
  const result = await blockstore.get(terminalCid, options)
80
- byteRangeContext.setBody(result)
81
- const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log }, {
81
+ context.byteRangeContext.setBody(result)
82
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log }, {
82
83
  redirected: false
83
84
  })
84
85
 
@@ -3,7 +3,7 @@ import toBrowserReadableStream from 'it-to-browser-readablestream'
3
3
  import { code as rawCode } from 'multiformats/codecs/raw'
4
4
  import { getETag } from '../utils/get-e-tag.js'
5
5
  import { tarStream } from '../utils/get-tar-stream.js'
6
- import { notAcceptableResponse, okResponse } from '../utils/responses.js'
6
+ import { notAcceptableResponse, okRangeResponse } from '../utils/responses.js'
7
7
  import { BasePlugin } from './plugin-base.js'
8
8
  import type { PluginContext } from './types.js'
9
9
 
@@ -13,12 +13,15 @@ import type { PluginContext } from './types.js'
13
13
  */
14
14
  export class TarPlugin extends BasePlugin {
15
15
  readonly codes = []
16
- canHandle ({ cid, accept, query }: PluginContext): boolean {
16
+ canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
17
17
  this.log('checking if we can handle %c with accept %s', cid, accept)
18
+ if (byteRangeContext == null) {
19
+ return false
20
+ }
18
21
  return accept === 'application/x-tar' || query.format === 'tar'
19
22
  }
20
23
 
21
- async handle (context: PluginContext): Promise<Response> {
24
+ async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
22
25
  const { cid, path, resource, options, pathDetails } = context
23
26
  const { getBlockstore } = this.pluginOptions
24
27
 
@@ -34,7 +37,9 @@ export class TarPlugin extends BasePlugin {
34
37
  const blockstore = getBlockstore(terminusElement, resource, options?.session, options)
35
38
  const stream = toBrowserReadableStream<Uint8Array>(tarStream(`/ipfs/${cid}/${path}`, blockstore, options))
36
39
 
37
- const response = okResponse(resource, stream)
40
+ context.byteRangeContext.setBody(stream)
41
+
42
+ const response = okRangeResponse(resource, context.byteRangeContext.getBody(), { byteRangeContext: context.byteRangeContext, log: this.log })
38
43
  response.headers.set('content-type', 'application/x-tar')
39
44
 
40
45
  response.headers.set('etag', getETag({ cid: terminusElement, reqFormat: context.reqFormat, weak: true }))
@@ -1,6 +1,7 @@
1
1
  import type { PluginError } from './errors.js'
2
2
  import type { VerifiedFetchInit } from '../index.js'
3
3
  import type { ContentTypeParser, RequestFormatShorthand } from '../types.js'
4
+ import type { ByteRangeContext } from '../utils/byte-range-context.js'
4
5
  import type { ParsedUrlStringResults } from '../utils/parse-url-string.js'
5
6
  import type { PathWalkerResponse } from '../utils/walk-path.js'
6
7
  import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
@@ -12,7 +13,7 @@ import type { CustomProgressEvent } from 'progress-events'
12
13
 
13
14
  /**
14
15
  * Contains common components and functions required by plugins to handle a request.
15
- * - Read-Only: Plugins can read but shouldnt rewrite them.
16
+ * - Read-Only: Plugins can read but shouldn't rewrite them.
16
17
  * - Persistent: Relevant even after the request completes (e.g., logging or metrics).
17
18
  */
18
19
  export interface PluginOptions {
@@ -47,6 +48,14 @@ export interface PluginContext extends ParsedUrlStringResults {
47
48
  errors?: PluginError[]
48
49
  reqFormat?: RequestFormatShorthand
49
50
  pathDetails?: PathWalkerResponse
51
+ query: ParsedUrlStringResults['query']
52
+ /**
53
+ * ByteRangeContext contains information about the size of the content and range requests.
54
+ * This can be used to set the Content-Length header without loading the entire body.
55
+ *
56
+ * This is set by the ByteRangeContextPlugin
57
+ */
58
+ byteRangeContext?: ByteRangeContext
50
59
  [key: string]: unknown
51
60
  }
52
61
 
@@ -107,6 +107,7 @@ export class ByteRangeContext {
107
107
  this.log.trace('returning body with byteStart=%o, byteEnd=%o, byteSize=%o', byteStart, byteEnd, byteSize)
108
108
  if (body instanceof ReadableStream) {
109
109
  // stream should already be spliced by `unixfs.cat`
110
+ // TODO: if the content is not unixfs and unixfs.cat was not called, we need to slice the body here.
110
111
  return body
111
112
  }
112
113
  return this.getSlicedBody(body)
@@ -144,10 +144,12 @@ function dnsLinkLabelDecoder (linkLabel: string): string {
144
144
  * A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties.
145
145
  *
146
146
  * After determining the protocol successfully, we process the cidOrPeerIdOrDnsLink:
147
- * * If it's ipfs, it parses the CID or throws an Aggregate error
148
- * * If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, an Aggregate error is thrown.
147
+ * * If it's ipfs, it parses the CID or throws Error[]
148
+ * * If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, Error[] is thrown.
149
149
  *
150
150
  * @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
151
+ *
152
+ * @throws {Error[]}
151
153
  */
152
154
  // eslint-disable-next-line complexity
153
155
  export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
@@ -185,12 +187,16 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
185
187
  // try resolving as an IPNS name
186
188
 
187
189
  peerId = getPeerIdFromString(cidOrPeerIdOrDnsLink)
188
- if (peerId.publicKey == null) {
190
+ const pubKey = peerId?.publicKey
191
+ if (pubKey == null) {
189
192
  throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
190
193
  }
191
194
 
192
195
  if (withServerTiming) {
193
- const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, ipns.resolve.bind(null, peerId.publicKey, options))
196
+ const resolveIpns = async (): Promise<IPNSResolveResult> => {
197
+ return ipns.resolve(pubKey, options)
198
+ }
199
+ const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, resolveIpns)
194
200
  serverTimings.push(resolveResultWithServerTiming)
195
201
 
196
202
  // eslint-disable-next-line max-depth
@@ -199,7 +205,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
199
205
  }
200
206
  resolveResult = resolveResultWithServerTiming.result
201
207
  } else {
202
- resolveResult = await ipns.resolve(peerId.publicKey, options)
208
+ resolveResult = await ipns.resolve(pubKey, options)
203
209
  }
204
210
  cid = resolveResult?.cid
205
211
  resolvedPath = resolveResult?.path
@@ -211,7 +217,7 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
211
217
  errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
212
218
  } else {
213
219
  log.error('could not resolve PeerId %c', peerId, err)
214
- errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
220
+ errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}": ${(err as Error).message}`))
215
221
  }
216
222
  }
217
223
 
@@ -255,7 +261,10 @@ export async function parseUrlString ({ urlString, ipns, logger, withServerTimin
255
261
  throw errors[0]
256
262
  }
257
263
 
258
- throw new AggregateError(errors, `Invalid resource. Cannot determine CID from URL "${urlString}"`)
264
+ errors.push(new Error(`Invalid resource. Cannot determine CID from URL "${urlString}".`))
265
+
266
+ // eslint-disable-next-line @typescript-eslint/no-throw-literal
267
+ throw errors
259
268
  }
260
269
 
261
270
  let ttl = calculateTtl(resolveResult)
@@ -98,17 +98,35 @@ export function notFoundResponse (url: string, body?: SupportedBodyTypes, init?:
98
98
  return response
99
99
  }
100
100
 
101
- /**
102
- * if body is an Error, it will be converted to a string containing the error message.
103
- */
104
- export function badRequestResponse (url: string, body?: SupportedBodyTypes | Error, init?: ResponseInit): Response {
105
- if (body instanceof Error) {
106
- body = body.message
101
+ function isArrayOfErrors (body: unknown | Error | Error[]): body is Error[] {
102
+ return Array.isArray(body) && body.every(e => e instanceof Error)
103
+ }
104
+
105
+ export function badRequestResponse (url: string, errors: Error | Error[], init?: ResponseInit): Response {
106
+ // stacktrace of the single error, or the stacktrace of the last error in the array
107
+ let stack: string | undefined
108
+ let convertedErrors: Array<{ message: string, stack: string }> | undefined
109
+ if (isArrayOfErrors(errors)) {
110
+ stack = errors[errors.length - 1].stack
111
+ convertedErrors = errors.map(e => ({ message: e.message, stack: e.stack ?? '' }))
112
+ } else if (errors instanceof Error) {
113
+ stack = errors.stack
114
+ convertedErrors = [{ message: errors.message, stack: errors.stack ?? '' }]
107
115
  }
108
- const response = new Response(body, {
109
- ...(init ?? {}),
116
+
117
+ const bodyJson = JSON.stringify({
118
+ stack,
119
+ errors: convertedErrors
120
+ })
121
+
122
+ const response = new Response(bodyJson, {
110
123
  status: 400,
111
- statusText: 'Bad Request'
124
+ statusText: 'Bad Request',
125
+ ...(init ?? {}),
126
+ headers: {
127
+ ...(init?.headers ?? {}),
128
+ 'Content-Type': 'application/json'
129
+ }
112
130
  })
113
131
 
114
132
  setType(response, 'basic')
@@ -4,6 +4,7 @@ import { prefixLogger } from '@libp2p/logger'
4
4
  import { LRUCache } from 'lru-cache'
5
5
  import { type CID } from 'multiformats/cid'
6
6
  import { CustomProgressEvent } from 'progress-events'
7
+ import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
7
8
  import { CarPlugin } from './plugins/plugin-handle-car.js'
8
9
  import { DagCborPlugin } from './plugins/plugin-handle-dag-cbor.js'
9
10
  import { DagPbPlugin } from './plugins/plugin-handle-dag-pb.js'
@@ -90,6 +91,7 @@ export class VerifiedFetch {
90
91
 
91
92
  const defaultPlugins = [
92
93
  new DagWalkPlugin(pluginOptions),
94
+ new ByteRangeContextPlugin(pluginOptions),
93
95
  new IpnsRecordPlugin(pluginOptions),
94
96
  new CarPlugin(pluginOptions),
95
97
  new RawPlugin(pluginOptions),
@@ -151,13 +153,24 @@ export class VerifiedFetch {
151
153
  * Server-Timing header to the response if it has been collected. It should be used for any final processing of the
152
154
  * response before it is returned to the user.
153
155
  */
154
- private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath, pathDetails }: Partial<PluginContext> = {}): Response {
156
+ private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath, pathDetails, byteRangeContext }: Partial<PluginContext> = {}): Response {
155
157
  if (this.serverTimingHeaders.length > 0) {
156
158
  const headerString = this.serverTimingHeaders.join(', ')
157
159
  response.headers.set('Server-Timing', headerString)
158
160
  this.serverTimingHeaders = []
159
161
  }
160
162
 
163
+ // if there are multiple ranges, we should omit the content-length header. see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding
164
+ if (response.headers.get('Transfer-Encoding') !== 'chunked') {
165
+ if (byteRangeContext != null) {
166
+ const contentLength = byteRangeContext.length
167
+ if (contentLength != null) {
168
+ this.log.trace('Setting Content-Length from byteRangeContext: %d', contentLength)
169
+ response.headers.set('Content-Length', contentLength.toString())
170
+ }
171
+ }
172
+ }
173
+
161
174
  // set Content-Disposition header
162
175
  let contentDisposition: string | undefined
163
176