@helia/verified-fetch 4.0.0 → 4.0.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 (96) hide show
  1. package/README.md +7 -49
  2. package/dist/index.min.js +52 -47
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/index.d.ts +8 -50
  5. package/dist/src/index.d.ts.map +1 -1
  6. package/dist/src/index.js +8 -50
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/plugins/index.d.ts +0 -1
  9. package/dist/src/plugins/index.d.ts.map +1 -1
  10. package/dist/src/plugins/index.js +0 -1
  11. package/dist/src/plugins/index.js.map +1 -1
  12. package/dist/src/plugins/plugin-base.d.ts.map +1 -1
  13. package/dist/src/plugins/plugin-base.js +3 -2
  14. package/dist/src/plugins/plugin-base.js.map +1 -1
  15. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  16. package/dist/src/plugins/plugin-handle-car.js +0 -1
  17. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  18. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
  19. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +0 -1
  20. package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
  21. package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -1
  22. package/dist/src/plugins/plugin-handle-dag-cbor.js +0 -1
  23. package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
  24. package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
  25. package/dist/src/plugins/plugin-handle-dag-pb.js +13 -16
  26. package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
  27. package/dist/src/plugins/plugin-handle-dag-walk.d.ts +8 -4
  28. package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
  29. package/dist/src/plugins/plugin-handle-dag-walk.js +8 -5
  30. package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
  31. package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
  32. package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
  33. package/dist/src/plugins/plugin-handle-ipns-record.js +3 -5
  34. package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
  35. package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
  36. package/dist/src/plugins/plugin-handle-json.js +0 -1
  37. package/dist/src/plugins/plugin-handle-json.js.map +1 -1
  38. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
  39. package/dist/src/plugins/plugin-handle-raw.js +3 -7
  40. package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
  41. package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -1
  42. package/dist/src/plugins/plugin-handle-tar.js +0 -1
  43. package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
  44. package/dist/src/plugins/types.d.ts +5 -7
  45. package/dist/src/plugins/types.d.ts.map +1 -1
  46. package/dist/src/utils/byte-range-context.d.ts +2 -2
  47. package/dist/src/utils/byte-range-context.d.ts.map +1 -1
  48. package/dist/src/utils/byte-range-context.js +1 -1
  49. package/dist/src/utils/byte-range-context.js.map +1 -1
  50. package/dist/src/utils/content-type-parser.d.ts.map +1 -1
  51. package/dist/src/utils/content-type-parser.js +0 -10
  52. package/dist/src/utils/content-type-parser.js.map +1 -1
  53. package/dist/src/utils/error-to-object.d.ts +6 -0
  54. package/dist/src/utils/error-to-object.d.ts.map +1 -0
  55. package/dist/src/utils/error-to-object.js +20 -0
  56. package/dist/src/utils/error-to-object.js.map +1 -0
  57. package/dist/src/utils/get-stream-from-async-iterable.d.ts +2 -2
  58. package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
  59. package/dist/src/utils/get-stream-from-async-iterable.js +2 -2
  60. package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
  61. package/dist/src/utils/responses.d.ts +2 -1
  62. package/dist/src/utils/responses.d.ts.map +1 -1
  63. package/dist/src/utils/responses.js +12 -1
  64. package/dist/src/utils/responses.js.map +1 -1
  65. package/dist/src/utils/walk-path.js +1 -1
  66. package/dist/src/utils/walk-path.js.map +1 -1
  67. package/dist/src/verified-fetch.d.ts.map +1 -1
  68. package/dist/src/verified-fetch.js +36 -28
  69. package/dist/src/verified-fetch.js.map +1 -1
  70. package/dist/typedoc-urls.json +0 -2
  71. package/package.json +2 -2
  72. package/src/index.ts +8 -50
  73. package/src/plugins/index.ts +0 -1
  74. package/src/plugins/plugin-base.ts +3 -2
  75. package/src/plugins/plugin-handle-car.ts +0 -2
  76. package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +2 -1
  77. package/src/plugins/plugin-handle-dag-cbor.ts +3 -1
  78. package/src/plugins/plugin-handle-dag-pb.ts +23 -15
  79. package/src/plugins/plugin-handle-dag-walk.ts +10 -5
  80. package/src/plugins/plugin-handle-ipns-record.ts +5 -5
  81. package/src/plugins/plugin-handle-json.ts +1 -1
  82. package/src/plugins/plugin-handle-raw.ts +6 -7
  83. package/src/plugins/plugin-handle-tar.ts +2 -1
  84. package/src/plugins/types.ts +6 -8
  85. package/src/utils/byte-range-context.ts +3 -3
  86. package/src/utils/content-type-parser.ts +5 -11
  87. package/src/utils/error-to-object.ts +22 -0
  88. package/src/utils/get-stream-from-async-iterable.ts +4 -4
  89. package/src/utils/responses.ts +15 -1
  90. package/src/utils/walk-path.ts +1 -1
  91. package/src/verified-fetch.ts +46 -28
  92. package/dist/src/plugins/errors.d.ts +0 -25
  93. package/dist/src/plugins/errors.d.ts.map +0 -1
  94. package/dist/src/plugins/errors.js +0 -33
  95. package/dist/src/plugins/errors.js.map +0 -1
  96. package/src/plugins/errors.ts +0 -37
@@ -3,7 +3,6 @@ import { code as rawCode } from 'multiformats/codecs/raw'
3
3
  import { identity } from 'multiformats/hashes/identity'
4
4
  import { getContentType } from '../utils/get-content-type.js'
5
5
  import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
6
- import { PluginFatalError } from './errors.js'
7
6
  import { BasePlugin } from './plugin-base.js'
8
7
  import type { PluginContext } from './types.js'
9
8
  import type { AcceptHeader } from '../utils/select-output-type.ts'
@@ -49,10 +48,10 @@ export class RawPlugin extends BasePlugin {
49
48
  codes: number[] = [rawCode, identity.code]
50
49
 
51
50
  canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
52
- this.log('checking if we can handle %c with accept %s', cid, accept)
53
51
  if (byteRangeContext == null) {
54
52
  return false
55
53
  }
54
+
56
55
  return accept?.mimeType === 'application/vnd.ipld.raw' || query.format === 'raw'
57
56
  }
58
57
 
@@ -66,21 +65,20 @@ export class RawPlugin extends BasePlugin {
66
65
  context.reqFormat = 'raw'
67
66
  context.query.download = true
68
67
  context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
69
- log.trace('Set content disposition...')
68
+ log.trace('set content disposition to force download')
70
69
  } else {
71
- log.trace('Did NOT set content disposition...')
70
+ log.trace('did not set content disposition, raw block will display inline')
72
71
  }
73
72
 
74
73
  if (path !== '' && cid.code === rawCode) {
75
74
  log.trace('404-ing raw codec request for %c/%s', cid, path)
76
- // throw new PluginError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths')
77
- // return notFoundResponse(resource, 'Raw codec does not support paths')
78
- throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
75
+ return notFoundResponse(resource)
79
76
  }
80
77
 
81
78
  const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
82
79
  const blockstore = getBlockstore(terminalCid, resource, session, options)
83
80
  const result = await toBuffer(blockstore.get(terminalCid, options))
81
+
84
82
  context.byteRangeContext.setBody(result)
85
83
 
86
84
  // if the user has specified an `Accept` header that corresponds to a raw
@@ -94,6 +92,7 @@ export class RawPlugin extends BasePlugin {
94
92
  contentTypeParser,
95
93
  log
96
94
  })
95
+
97
96
  const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
98
97
  redirected: false
99
98
  })
@@ -14,11 +14,12 @@ import type { PluginContext } from './types.js'
14
14
  export class TarPlugin extends BasePlugin {
15
15
  readonly id = 'tar-plugin'
16
16
  readonly codes = []
17
+
17
18
  canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
18
- this.log('checking if we can handle %c with accept %s', cid, accept)
19
19
  if (byteRangeContext == null) {
20
20
  return false
21
21
  }
22
+
22
23
  return accept?.mimeType === 'application/x-tar' || query.format === 'tar'
23
24
  }
24
25
 
@@ -1,11 +1,10 @@
1
- import type { PluginError } from './errors.js'
2
1
  import type { ResolveURLResult, UrlQuery, VerifiedFetchInit, ContentTypeParser, RequestFormatShorthand } from '../index.js'
3
2
  import type { ByteRangeContext } from '../utils/byte-range-context.js'
4
3
  import type { AcceptHeader } from '../utils/select-output-type.ts'
5
4
  import type { ServerTiming } from '../utils/server-timing.ts'
6
5
  import type { PathWalkerResponse } from '../utils/walk-path.js'
7
6
  import type { IPNSResolver } from '@helia/ipns'
8
- import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
7
+ import type { AbortOptions, Logger } from '@libp2p/interface'
9
8
  import type { Helia } from 'helia'
10
9
  import type { Blockstore } from 'interface-blockstore'
11
10
  import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
@@ -18,7 +17,7 @@ import type { CustomProgressEvent } from 'progress-events'
18
17
  * - Persistent: Relevant even after the request completes (e.g., logging or metrics).
19
18
  */
20
19
  export interface PluginOptions {
21
- logger: ComponentLogger
20
+ logger: Logger
22
21
  getBlockstore(cid: CID, resource: string | CID, useSession?: boolean, options?: AbortOptions): Blockstore
23
22
  contentTypeParser?: ContentTypeParser
24
23
  helia: Helia
@@ -51,7 +50,6 @@ export interface PluginContext extends ResolveURLResult {
51
50
  options?: Omit<VerifiedFetchInit, 'signal'> & AbortOptions
52
51
  isDirectory?: boolean
53
52
  directoryEntries?: UnixFSEntry[]
54
- errors?: PluginError[]
55
53
  reqFormat?: RequestFormatShorthand
56
54
  pathDetails?: PathWalkerResponse
57
55
  query: UrlQuery
@@ -65,6 +63,10 @@ export interface PluginContext extends ResolveURLResult {
65
63
  byteRangeContext?: ByteRangeContext
66
64
  serverTiming: ServerTiming
67
65
  ipfsPath: string
66
+
67
+ /**
68
+ * Allow arbitrary keys/values
69
+ */
68
70
  [key: string]: unknown
69
71
  }
70
72
 
@@ -85,7 +87,3 @@ export interface PluginErrorOptions {
85
87
  details?: Record<string, any>
86
88
  response?: Response
87
89
  }
88
-
89
- export interface FatalPluginErrorOptions extends PluginErrorOptions {
90
- response: Response
91
- }
@@ -3,7 +3,7 @@ import { InvalidRangeError } from '../errors.js'
3
3
  import { calculateByteRangeIndexes, getHeader } from './request-headers.js'
4
4
  import { getContentRangeHeader } from './response-headers.js'
5
5
  import type { SupportedBodyTypes } from '../index.js'
6
- import type { ComponentLogger, Logger } from '@libp2p/interface'
6
+ import type { Logger } from '@libp2p/interface'
7
7
 
8
8
  type SliceableBody = Exclude<SupportedBodyTypes, ReadableStream<Uint8Array> | null>
9
9
 
@@ -90,8 +90,8 @@ export class ByteRangeContext {
90
90
  // to be set by isValidRangeRequest so that we don't need to re-check the byteRanges
91
91
  private _isValidRangeRequest: boolean = false
92
92
 
93
- constructor (logger: ComponentLogger, private readonly headers?: HeadersInit) {
94
- this.log = logger.forComponent('helia:verified-fetch:byte-range-context')
93
+ constructor (logger: Logger, private readonly headers?: HeadersInit) {
94
+ this.log = logger.newScope('byte-range-context')
95
95
  this.rangeRequestHeader = getHeader(this.headers, 'Range')
96
96
 
97
97
  if (this.rangeRequestHeader != null) {
@@ -1,28 +1,22 @@
1
- import { logger } from '@libp2p/logger'
2
1
  import { fileTypeFromBuffer } from 'file-type'
3
2
 
4
- const log = logger('helia:verified-fetch:content-type-parser')
5
-
6
3
  export const defaultMimeType = 'application/octet-stream'
7
4
  function checkForSvg (text: string): boolean {
8
- log('checking for svg')
9
5
  return /^(<\?xml[^>]+>)?[^<^\w]+<svg/ig.test(text)
10
6
  }
11
7
 
12
8
  async function checkForJson (text: string): Promise<boolean> {
13
- log('checking for json')
14
9
  try {
15
10
  JSON.parse(text)
16
11
  return true
17
12
  } catch (err) {
18
- log('failed to parse as json', err)
19
13
  return false
20
14
  }
21
15
  }
22
16
 
23
17
  function getText (bytes: Uint8Array): string | null {
24
- log('checking for text')
25
18
  const decoder = new TextDecoder('utf-8', { fatal: true })
19
+
26
20
  try {
27
21
  return decoder.decode(bytes)
28
22
  } catch (err) {
@@ -31,25 +25,24 @@ function getText (bytes: Uint8Array): string | null {
31
25
  }
32
26
 
33
27
  async function checkForHtml (text: string): Promise<boolean> {
34
- log('checking for html')
35
28
  return /^\s*<(?:!doctype\s+html|html|head|body)\b/i.test(text)
36
29
  }
37
30
 
38
31
  export async function contentTypeParser (bytes: Uint8Array, fileName?: string): Promise<string> {
39
- log('contentTypeParser called for fileName: %s, byte size=%s', fileName, bytes.length)
40
32
  const detectedType = (await fileTypeFromBuffer(bytes))?.mime
33
+
41
34
  if (detectedType != null) {
42
- log('detectedType: %s', detectedType)
43
35
  if (detectedType === 'application/xml' && fileName?.toLowerCase().endsWith('.svg')) {
44
36
  return 'image/svg+xml'
45
37
  }
38
+
46
39
  return detectedType
47
40
  }
48
- log('no detectedType')
49
41
 
50
42
  if (fileName == null) {
51
43
  // it's likely text... no other way to determine file-type.
52
44
  const text = getText(bytes)
45
+
53
46
  if (text != null) {
54
47
  // check for svg, json, html, or it's plain text.
55
48
  if (checkForSvg(text)) {
@@ -62,6 +55,7 @@ export async function contentTypeParser (bytes: Uint8Array, fileName?: string):
62
55
  return 'text/plain; charset=utf-8'
63
56
  }
64
57
  }
58
+
65
59
  return defaultMimeType
66
60
  }
67
61
 
@@ -0,0 +1,22 @@
1
+ function isAggregateError (err?: any): err is AggregateError {
2
+ return err instanceof AggregateError || (err?.name === 'AggregateError' && Array.isArray(err?.errors))
3
+ }
4
+
5
+ /**
6
+ * Error instance properties are not enumerable so we must transform the error
7
+ * into a plain object if we want to pass it to `JSON.stringify` or similar.
8
+ */
9
+ export function errorToObject (err: Error): any {
10
+ let errors
11
+
12
+ if (isAggregateError(err)) {
13
+ errors = err.errors.map(err => errorToObject(err))
14
+ }
15
+
16
+ return {
17
+ name: err.name,
18
+ message: err.message,
19
+ stack: err.stack,
20
+ errors
21
+ }
22
+ }
@@ -2,18 +2,18 @@ import { AbortError } from '@libp2p/interface'
2
2
  import { CustomProgressEvent } from 'progress-events'
3
3
  import { NoContentError } from '../errors.js'
4
4
  import type { VerifiedFetchInit } from '../index.js'
5
- import type { ComponentLogger } from '@libp2p/interface'
5
+ import type { Logger } from '@libp2p/interface'
6
6
 
7
7
  /**
8
8
  * Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
9
9
  */
10
- export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: ComponentLogger, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
11
- const log = logger.forComponent('helia:verified-fetch:get-stream-from-async-iterable')
10
+ export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, path: string, logger: Logger, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
11
+ const log = logger.newScope('get-stream-from-async-iterable')
12
12
  const reader = iterator[Symbol.asyncIterator]()
13
13
  const { value: firstChunk, done } = await reader.next()
14
14
 
15
15
  if (done === true) {
16
- log.error('no content found for path', path)
16
+ log.error('no content found for path "%s"', path)
17
17
  throw new NoContentError()
18
18
  }
19
19
 
@@ -44,6 +44,20 @@ export function okResponse (url: string, body?: SupportedBodyTypes, init?: Respo
44
44
  return response
45
45
  }
46
46
 
47
+ export function internalServerErrorResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
48
+ const response = new Response(body, {
49
+ ...(init ?? {}),
50
+ status: 500,
51
+ statusText: 'Internal Server Error'
52
+ })
53
+ response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
54
+
55
+ setType(response, 'basic')
56
+ setUrl(response, url)
57
+
58
+ return response
59
+ }
60
+
47
61
  export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
48
62
  const response = new Response(body, {
49
63
  ...(init ?? {}),
@@ -57,7 +71,7 @@ export function badGatewayResponse (url: string, body?: SupportedBodyTypes, init
57
71
  return response
58
72
  }
59
73
 
60
- export function notSupportedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
74
+ export function notImplementedResponse (url: string, body?: SupportedBodyTypes, init?: ResponseInit): Response {
61
75
  const response = new Response(body, {
62
76
  ...(init ?? {}),
63
77
  status: 501,
@@ -52,7 +52,7 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
52
52
  */
53
53
  export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
54
54
  try {
55
- return await walkPath(blockstore, `${cid.toString()}/${path}`, options)
55
+ return await walkPath(blockstore, `${cid}/${path}`, options)
56
56
  } catch (err: any) {
57
57
  if (options?.signal?.aborted) {
58
58
  throw new AbortError(options?.signal?.reason)
@@ -1,7 +1,6 @@
1
1
  import { dnsLink } from '@helia/dnslink'
2
2
  import { ipnsResolver } from '@helia/ipns'
3
3
  import { AbortError } from '@libp2p/interface'
4
- import { prefixLogger } from '@libp2p/logger'
5
4
  import { CustomProgressEvent } from 'progress-events'
6
5
  import QuickLRU from 'quick-lru'
7
6
  import { ByteRangeContextPlugin } from './plugins/plugin-handle-byte-range-context.js'
@@ -15,6 +14,7 @@ import { RawPlugin } from './plugins/plugin-handle-raw.js'
15
14
  import { TarPlugin } from './plugins/plugin-handle-tar.js'
16
15
  import { URLResolver } from './url-resolver.ts'
17
16
  import { contentTypeParser } from './utils/content-type-parser.js'
17
+ import { errorToObject } from './utils/error-to-object.ts'
18
18
  import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
19
19
  import { getETag } from './utils/get-e-tag.js'
20
20
  import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
@@ -22,7 +22,7 @@ import { getRedirectResponse } from './utils/handle-redirects.js'
22
22
  import { uriEncodeIPFSPath } from './utils/ipfs-path-to-string.ts'
23
23
  import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
24
24
  import { setCacheControlHeader } from './utils/response-headers.js'
25
- import { badRequestResponse, notAcceptableResponse, notSupportedResponse, badGatewayResponse } from './utils/responses.js'
25
+ import { badRequestResponse, notAcceptableResponse, internalServerErrorResponse, notImplementedResponse } from './utils/responses.js'
26
26
  import { selectOutputType } from './utils/select-output-type.js'
27
27
  import { ServerTiming } from './utils/server-timing.js'
28
28
  import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, ResolveURLResult, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
@@ -83,7 +83,7 @@ export class VerifiedFetch {
83
83
 
84
84
  const pluginOptions: PluginOptions = {
85
85
  ...init,
86
- logger: prefixLogger('helia:verified-fetch'),
86
+ logger: helia.logger.forComponent('verified-fetch'),
87
87
  getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
88
88
  helia,
89
89
  contentTypeParser: this.contentTypeParser,
@@ -116,8 +116,6 @@ export class VerifiedFetch {
116
116
  } else {
117
117
  this.plugins = defaultPlugins
118
118
  }
119
-
120
- this.log.trace('created VerifiedFetch instance')
121
119
  }
122
120
 
123
121
  private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
@@ -160,30 +158,26 @@ export class VerifiedFetch {
160
158
  // set Content-Disposition header
161
159
  let contentDisposition: string | undefined
162
160
 
163
- this.log.trace('checking for content disposition')
164
-
165
161
  // force download if requested
166
162
  if (context?.query?.download === true) {
163
+ this.log.trace('download requested')
167
164
  contentDisposition = 'attachment'
168
- } else {
169
- this.log.trace('download not requested')
170
165
  }
171
166
 
172
167
  // override filename if requested
173
168
  if (context?.query?.filename != null) {
169
+ this.log.trace('specific filename requested')
170
+
174
171
  if (contentDisposition == null) {
175
172
  contentDisposition = 'inline'
176
173
  }
177
174
 
178
175
  contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(context.query.filename)}`
179
- } else {
180
- this.log.trace('no filename specified in query')
181
176
  }
182
177
 
183
178
  if (contentDisposition != null) {
179
+ this.log.trace('content disposition %s', contentDisposition)
184
180
  response.headers.set('Content-Disposition', contentDisposition)
185
- } else {
186
- this.log.trace('no content disposition specified')
187
181
  }
188
182
 
189
183
  if (context?.cid != null && response.headers.get('etag') == null) {
@@ -240,7 +234,7 @@ export class VerifiedFetch {
240
234
  * Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
241
235
  * we re-check `canHandle()` for all plugins in the next iteration if the context changed.
242
236
  */
243
- private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response | undefined> {
237
+ private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response> {
244
238
  let finalResponse: Response | undefined
245
239
  let passCount = 0
246
240
  const pluginsUsed = new Set<string>()
@@ -251,20 +245,24 @@ export class VerifiedFetch {
251
245
  this.log(`starting pipeline pass #${passCount + 1}`)
252
246
  passCount++
253
247
 
248
+ this.log.trace('checking which plugins can handle %c%s with accept %o', context.cid, context.path != null ? `/${context.path}` : '', context.accept)
249
+
254
250
  // gather plugins that say they can handle the *current* context, but haven't been used yet
255
251
  const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.id)).filter(p => p.canHandle(context))
252
+
256
253
  if (readyPlugins.length === 0) {
257
- this.log.trace('no plugins can handle the current context.. checking by CID code')
254
+ this.log.trace('no plugins can handle the current context, checking by CID code')
258
255
  const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
256
+
259
257
  if (plugins.length > 0) {
260
258
  readyPlugins.push(...plugins)
261
259
  } else {
262
- this.log.trace('no plugins found that can handle request by CID code; exiting pipeline.')
260
+ this.log.trace('no plugins found that can handle request by CID code; exiting pipeline')
263
261
  break
264
262
  }
265
263
  }
266
264
 
267
- this.log.trace('plugins ready to handle request: ', readyPlugins.map(p => p.id).join(', '))
265
+ this.log.trace('plugins ready to handle request: %s', readyPlugins.map(p => p.id).join(', '))
268
266
 
269
267
  // track if any plugin changed the context or returned a response
270
268
  let contextChanged = false
@@ -272,10 +270,13 @@ export class VerifiedFetch {
272
270
 
273
271
  for (const plugin of readyPlugins) {
274
272
  try {
275
- this.log.trace('invoking plugin:', plugin.id)
273
+ this.log('invoking plugin: %s', plugin.id)
276
274
  pluginsUsed.add(plugin.id)
277
275
 
278
276
  const maybeResponse = await plugin.handle(context)
277
+
278
+ this.log('plugin response %s %o', plugin.id, maybeResponse)
279
+
279
280
  if (maybeResponse != null) {
280
281
  // if a plugin returns a final Response, short-circuit
281
282
  finalResponse = maybeResponse
@@ -286,12 +287,16 @@ export class VerifiedFetch {
286
287
  if (context.options?.signal?.aborted) {
287
288
  throw new AbortError(context.options?.signal?.reason)
288
289
  }
289
- this.log.error('error in plugin %s - %e', plugin.constructor.name, err)
290
- // if fatal, short-circuit the pipeline
291
- if (err.name === 'PluginFatalError') {
292
- // if plugin provides a custom error response, return it
293
- return err.response ?? badGatewayResponse(context.resource, 'Failed to fetch')
294
- }
290
+
291
+ this.log.error('error in plugin %s - %e', plugin.id, err)
292
+
293
+ return internalServerErrorResponse(context.resource, JSON.stringify({
294
+ error: errorToObject(err)
295
+ }), {
296
+ headers: {
297
+ 'content-type': 'application/json'
298
+ }
299
+ })
295
300
  } finally {
296
301
  // on each plugin call, check for changes in the context
297
302
  const newModificationId = context.modified
@@ -317,7 +322,13 @@ export class VerifiedFetch {
317
322
  }
318
323
  }
319
324
 
320
- return finalResponse
325
+ return finalResponse ?? notImplementedResponse(context.resource, JSON.stringify({
326
+ error: errorToObject(new Error('No verified fetch plugin could handle the request'))
327
+ }), {
328
+ headers: {
329
+ 'content-type': 'application/json'
330
+ }
331
+ })
321
332
  }
322
333
 
323
334
  /**
@@ -363,7 +374,7 @@ export class VerifiedFetch {
363
374
  const acceptHeader = getResolvedAcceptHeader({ query: parsedResult.query, headers: options?.headers, logger: this.helia.logger })
364
375
 
365
376
  const accept: AcceptHeader | undefined = selectOutputType(parsedResult.cid, acceptHeader)
366
- this.log('output type %s', accept)
377
+ this.log('accept %o', accept)
367
378
 
368
379
  if (acceptHeader != null && accept == null) {
369
380
  this.log.error('could not fulfil request based on accept header')
@@ -394,9 +405,16 @@ export class VerifiedFetch {
394
405
 
395
406
  const response = await this.runPluginPipeline(context)
396
407
 
397
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: parsedResult.cid, path: parsedResult.path }))
408
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', {
409
+ cid: parsedResult.cid,
410
+ path: parsedResult.path
411
+ }))
412
+
413
+ if (response == null) {
414
+ this.log.error('no plugin could handle request for %s', resource)
415
+ }
398
416
 
399
- return this.handleFinalResponse(response ?? notSupportedResponse(resource.toString()), context)
417
+ return this.handleFinalResponse(response, context)
400
418
  }
401
419
 
402
420
  /**
@@ -1,25 +0,0 @@
1
- import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js';
2
- /**
3
- * If a plugin encounters an error, it should throw an instance of this class.
4
- */
5
- export declare class PluginError extends Error {
6
- name: string;
7
- code: string;
8
- fatal: boolean;
9
- details?: Record<string, any>;
10
- response?: any;
11
- constructor(code: string, message: string, options?: PluginErrorOptions);
12
- }
13
- /**
14
- * If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
15
- * an instance of this class.
16
- *
17
- * Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
18
- * processed further. If you do not have a response to return to the client, you should consider throwing a
19
- * `PluginError` instead.
20
- */
21
- export declare class PluginFatalError extends PluginError {
22
- name: string;
23
- constructor(code: string, message: string, options: FatalPluginErrorOptions);
24
- }
25
- //# sourceMappingURL=errors.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE7E;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAC7B,IAAI,SAAgB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,QAAQ,CAAC,EAAE,GAAG,CAAA;gBAER,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB;CAOzE;AAED;;;;;;;GAOG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;IACxC,IAAI,SAAqB;gBAEnB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB;CAI7E"}
@@ -1,33 +0,0 @@
1
- /**
2
- * If a plugin encounters an error, it should throw an instance of this class.
3
- */
4
- export class PluginError extends Error {
5
- name = 'PluginError';
6
- code;
7
- fatal;
8
- details;
9
- response;
10
- constructor(code, message, options) {
11
- super(message);
12
- this.code = code;
13
- this.fatal = options?.fatal ?? false;
14
- this.details = options?.details;
15
- this.response = options?.response;
16
- }
17
- }
18
- /**
19
- * If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
20
- * an instance of this class.
21
- *
22
- * Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
23
- * processed further. If you do not have a response to return to the client, you should consider throwing a
24
- * `PluginError` instead.
25
- */
26
- export class PluginFatalError extends PluginError {
27
- name = 'PluginFatalError';
28
- constructor(code, message, options) {
29
- super(code, message, { ...options, fatal: true });
30
- this.name = 'PluginFatalError';
31
- }
32
- }
33
- //# sourceMappingURL=errors.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/plugins/errors.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC7B,IAAI,GAAG,aAAa,CAAA;IACpB,IAAI,CAAQ;IACZ,KAAK,CAAS;IACd,OAAO,CAAsB;IAC7B,QAAQ,CAAM;IAErB,YAAa,IAAY,EAAE,OAAe,EAAE,OAA4B;QACtE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,KAAK,CAAA;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,CAAA;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,CAAA;IACnC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAiB,SAAQ,WAAW;IACxC,IAAI,GAAG,kBAAkB,CAAA;IAEhC,YAAa,IAAY,EAAE,OAAe,EAAE,OAAgC;QAC1E,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAA;IAChC,CAAC;CACF"}
@@ -1,37 +0,0 @@
1
- import type { FatalPluginErrorOptions, PluginErrorOptions } from './types.js'
2
-
3
- /**
4
- * If a plugin encounters an error, it should throw an instance of this class.
5
- */
6
- export class PluginError extends Error {
7
- public name = 'PluginError'
8
- public code: string
9
- public fatal: boolean
10
- public details?: Record<string, any>
11
- public response?: any
12
-
13
- constructor (code: string, message: string, options?: PluginErrorOptions) {
14
- super(message)
15
- this.code = code
16
- this.fatal = options?.fatal ?? false
17
- this.details = options?.details
18
- this.response = options?.response
19
- }
20
- }
21
-
22
- /**
23
- * If a plugin encounters a fatal error and verified-fetch should not continue processing the request, it should throw
24
- * an instance of this class.
25
- *
26
- * Note that you should be very careful when throwing a `PluginFatalError`, as it will stop the request from being
27
- * processed further. If you do not have a response to return to the client, you should consider throwing a
28
- * `PluginError` instead.
29
- */
30
- export class PluginFatalError extends PluginError {
31
- public name = 'PluginFatalError'
32
-
33
- constructor (code: string, message: string, options: FatalPluginErrorOptions) {
34
- super(code, message, { ...options, fatal: true })
35
- this.name = 'PluginFatalError'
36
- }
37
- }