@helia/verified-fetch 5.0.2 → 5.0.4

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.
@@ -53,7 +53,7 @@ export class CarPlugin extends BasePlugin {
53
53
  }
54
54
 
55
55
  async handle (context: PluginContext): Promise<Response> {
56
- const { options, url, accept, resource, blockstore, range, ipfsRoots, terminalElement } = context
56
+ const { options, url, accept, resource, blockstore, range, ipfsRoots, terminalElement, requestedMimeTypes } = context
57
57
 
58
58
  if (range != null) {
59
59
  return badRequestResponse(resource, new Error('Range requests are not supported for CAR files'))
@@ -67,12 +67,12 @@ export class CarPlugin extends BasePlugin {
67
67
  return badRequestResponse(resource, new Error('Could not find CAR media type in accept header'))
68
68
  }
69
69
 
70
- const order = acceptCar.options.order === 'dfs' || url.searchParams.get('car-order') === 'dfs' ? 'dfs' : 'unk'
71
- const duplicates = acceptCar.options.dups !== 'n' && url.searchParams.get('car-dups') !== 'n'
70
+ const order = acceptCar.options.order === 'dfs' ? 'dfs' : 'unk'
71
+ const duplicates = acceptCar.options.dups !== 'n'
72
72
 
73
73
  // TODO: `@ipld/car` only supports CARv1
74
74
  if (acceptCar.options.version === '2' || url.searchParams.get('car-version') === '2') {
75
- return notAcceptableResponse(resource, [
75
+ return notAcceptableResponse(resource, requestedMimeTypes, [
76
76
  CONTENT_TYPE_CAR
77
77
  ])
78
78
  }
@@ -43,7 +43,7 @@ export class IpldPlugin extends BasePlugin {
43
43
  }
44
44
 
45
45
  async handle (context: PluginContext): Promise<Response> {
46
- const { url, resource, accept, ipfsRoots, terminalElement, blockstore, options } = context
46
+ const { url, resource, accept, ipfsRoots, terminalElement, blockstore, options, requestedMimeTypes } = context
47
47
 
48
48
  this.log.trace('fetching %c/%s', terminalElement.cid, url.pathname)
49
49
  let block: Uint8Array
@@ -64,7 +64,7 @@ export class IpldPlugin extends BasePlugin {
64
64
  contentType = result.contentType
65
65
  } catch (err) {
66
66
  this.log.error('could not decode object from block - %e', err)
67
- return notAcceptableResponse(resource, getContentTypesForCid(terminalElement.cid))
67
+ return notAcceptableResponse(resource, requestedMimeTypes, getContentTypesForCid(terminalElement.cid))
68
68
  }
69
69
 
70
70
  const headers = {
@@ -1,5 +1,4 @@
1
- import { CONTENT_TYPE_OCTET_STREAM, CONTENT_TYPE_RAW } from './content-types.ts'
2
- import { badGatewayResponse, gatewayTimeoutResponse, internalServerErrorResponse, notAcceptableResponse, notFoundResponse, preconditionFailedResponse } from './responses.js'
1
+ import { badGatewayResponse, gatewayTimeoutResponse, internalServerErrorResponse, notFoundResponse, preconditionFailedResponse } from './responses.js'
3
2
  import type { Resource } from '../index.js'
4
3
 
5
4
  export function errorToResponse (resource: Resource | string, err: any): Response {
@@ -10,15 +9,12 @@ export function errorToResponse (resource: Resource | string, err: any): Respons
10
9
 
11
10
  // could not reach an upstream server, bad connection or offline
12
11
  if (err.code === 'ECONNREFUSED' || err.code === 'ECANCELLED' || err.name === 'DNSQueryFailedError') {
13
- return badGatewayResponse(resource.toString(), err)
12
+ return gatewayTimeoutResponse(resource.toString(), err)
14
13
  }
15
14
 
16
15
  // data was not parseable, user may be able to request raw block
17
16
  if (['NotUnixFSError'].includes(err.name)) {
18
- return notAcceptableResponse(resource.toString(), [
19
- CONTENT_TYPE_RAW,
20
- CONTENT_TYPE_OCTET_STREAM
21
- ])
17
+ return badGatewayResponse(resource.toString(), err)
22
18
  }
23
19
 
24
20
  // an upstream server didn't respond in time but inside the signal timeout
@@ -40,8 +36,8 @@ export function errorToResponse (resource: Resource | string, err: any): Respons
40
36
  return preconditionFailedResponse(resource.toString())
41
37
  }
42
38
 
43
- if (['RecordNotFoundError'].includes('err.name')) {
44
- return badGatewayResponse(resource.toString(), err)
39
+ if (['RecordNotFoundError', 'LoadBlockFailedError'].includes(err.name)) {
40
+ return gatewayTimeoutResponse(resource.toString(), err)
45
41
  }
46
42
 
47
43
  // can't tell what went wrong, return a generic error
@@ -119,11 +119,12 @@ export function notImplementedResponse (url: string, body?: SupportedBodyTypes,
119
119
  return response
120
120
  }
121
121
 
122
- export function notAcceptableResponse (url: string | URL, acceptable: ContentType[], init?: ResponseInit): Response {
122
+ export function notAcceptableResponse (url: string | URL, requested: Array<Pick<ContentType, 'mediaType'>>, acceptable: Array<Pick<ContentType, 'mediaType'>>, init?: ResponseInit): Response {
123
123
  const headers = new Headers(init?.headers)
124
124
  headers.set('content-type', 'application/json')
125
125
 
126
126
  const response = new Response(JSON.stringify({
127
+ requested: requested.map(contentType => contentType.mediaType),
127
128
  acceptable: acceptable.map(contentType => contentType.mediaType)
128
129
  }), {
129
130
  ...(init ?? {}),
@@ -12,7 +12,7 @@ import { TarPlugin } from './plugins/plugin-handle-tar.js'
12
12
  import { UnixFSPlugin } from './plugins/plugin-handle-unixfs.js'
13
13
  import { URLResolver } from './url-resolver.ts'
14
14
  import { contentTypeParser } from './utils/content-type-parser.js'
15
- import { getContentType, getSupportedContentTypes, CONTENT_TYPE_OCTET_STREAM, CONTENT_TYPE_CAR, MEDIA_TYPE_IPNS_RECORD, MEDIA_TYPE_RAW } from './utils/content-types.ts'
15
+ import { getContentType, getSupportedContentTypes, CONTENT_TYPE_OCTET_STREAM, MEDIA_TYPE_IPNS_RECORD, MEDIA_TYPE_RAW, CONTENT_TYPE_IPNS } from './utils/content-types.ts'
16
16
  import { errorToObject } from './utils/error-to-object.ts'
17
17
  import { errorToResponse } from './utils/error-to-response.ts'
18
18
  import { getETag } from './utils/get-e-tag.js'
@@ -165,20 +165,25 @@ export class VerifiedFetch {
165
165
  return this.handleFinalResponse(badRequestResponse(resource.toString(), err))
166
166
  }
167
167
 
168
+ const requestedMimeTypes = getRequestedMimeTypes(url, headers.get('accept'))
169
+
168
170
  let parsedResult: ResolveURLResult
169
171
 
170
172
  // if just an IPNS record has been requested, don't try to load the block
171
173
  // the record points to or do any recursive IPNS resolving
172
174
  if (isIPNSRecordRequest(headers)) {
173
175
  if (url.protocol !== 'ipns:') {
174
- return notAcceptableResponse(url, [])
176
+ return notAcceptableResponse(url, requestedMimeTypes, [
177
+ CONTENT_TYPE_IPNS
178
+ ])
175
179
  }
176
180
 
177
181
  // @ts-expect-error ipnsRecordPlugin may not be of type IpnsRecordPlugin
178
182
  const ipnsRecordPlugin: IpnsRecordPlugin | undefined = this.plugins.find(plugin => plugin.id === 'ipns-record-plugin')
179
183
 
180
184
  if (ipnsRecordPlugin == null) {
181
- return notAcceptableResponse(url, [])
185
+ // IPNS record was requested but no IPNS Record plugin is configured?!
186
+ return notAcceptableResponse(url, requestedMimeTypes, [])
182
187
  }
183
188
 
184
189
  return this.handleFinalResponse(await ipnsRecordPlugin.handle({
@@ -198,6 +203,7 @@ export class VerifiedFetch {
198
203
  options?.signal?.throwIfAborted()
199
204
 
200
205
  this.log.error('error parsing resource %s - %e', resource, err)
206
+ this.log.error('wat name %s', err.name)
201
207
  return this.handleFinalResponse(errorToResponse(resource, err))
202
208
  }
203
209
  }
@@ -207,7 +213,7 @@ export class VerifiedFetch {
207
213
  path: parsedResult.url.pathname
208
214
  }))
209
215
 
210
- const accept = this.getAcceptHeader(parsedResult.url, headers.get('accept'), parsedResult.terminalElement.cid)
216
+ const accept = this.getAcceptHeader(parsedResult.url, requestedMimeTypes, parsedResult.terminalElement.cid)
211
217
 
212
218
  if (accept instanceof Response) {
213
219
  this.log('allowed media types for requested CID did not contain anything the client can understand')
@@ -224,7 +230,8 @@ export class VerifiedFetch {
224
230
  options,
225
231
  onProgress: options?.onProgress,
226
232
  serverTiming,
227
- headers
233
+ headers,
234
+ requestedMimeTypes
228
235
  }
229
236
 
230
237
  this.log.trace('finding handler for cid code "0x%s" and response content types %s', parsedResult.terminalElement.cid.code.toString(16), accept.map(header => header.contentType.mediaType).join(', '))
@@ -247,85 +254,9 @@ export class VerifiedFetch {
247
254
  * Returns a prioritized list of acceptable content types for the response
248
255
  * based on the CID and a passed `Accept` header
249
256
  */
250
- private getAcceptHeader (url: URL, accept?: string | null, cid?: CID): AcceptHeader[] | Response {
251
- if (accept == null || accept === '') {
252
- // if the user has specified CAR options but no Accept header, default to
253
- // the car content type with the passed options
254
- try {
255
- const dagScope = url.searchParams.get('dag-scope')
256
- const entityBytes = url.searchParams.get('entity-bytes')
257
- const dups = url.searchParams.get('car-dups')
258
- const order = url.searchParams.get('car-order')
259
- const version = url.searchParams.get('car-version')
260
-
261
- if (dagScope != null ||
262
- entityBytes != null ||
263
- dups != null ||
264
- entityBytes != null ||
265
- order != null
266
- ) {
267
- const options: Record<string, string> = {}
268
-
269
- if (dups != null) {
270
- options.dups = dups
271
- }
272
-
273
- if (order != null) {
274
- options.order = order
275
- }
276
-
277
- if (version != null) {
278
- options.version = version
279
- }
280
-
281
- return [{
282
- contentType: CONTENT_TYPE_CAR,
283
- options
284
- }]
285
- }
286
- } catch {}
287
-
288
- // yolo content-type
289
- accept = '*/*'
290
- // return []
291
- }
292
-
293
- // allow user to choose specific output type
294
- const acceptable: AcceptHeader[] = []
295
-
296
- const requestedMimeTypes = accept
297
- .split(',')
298
- .map(s => {
299
- const parts = s.trim().split(';')
300
-
301
- const options: Record<string, string> = {
302
- q: '1'
303
- }
304
-
305
- for (let i = 1; i < parts.length; i++) {
306
- const [key, value] = parts[i].split('=').map(s => s.trim())
307
-
308
- options[key] = value
309
- }
310
-
311
- return {
312
- mediaType: `${parts[0]}`.trim(),
313
- options
314
- }
315
- })
316
- .sort((a, b) => {
317
- if (a.options.q === b.options.q) {
318
- return 0
319
- }
320
-
321
- if (a.options.q > b.options.q) {
322
- return -1
323
- }
324
-
325
- return 1
326
- })
327
-
257
+ private getAcceptHeader (url: URL, requestedMimeTypes: RequestedMimeType[], cid?: CID): AcceptHeader[] | Response {
328
258
  const supportedContentTypes = getSupportedContentTypes(url.protocol, cid)
259
+ const acceptable: AcceptHeader[] = []
329
260
 
330
261
  for (const headerFormat of requestedMimeTypes) {
331
262
  const [headerFormatType, headerFormatSubType] = headerFormat.mediaType.split('/')
@@ -367,7 +298,7 @@ export class VerifiedFetch {
367
298
  this.log('requested %o', requestedMimeTypes.map(({ mediaType }) => mediaType))
368
299
  this.log('supported %o', supportedContentTypes.map(({ mediaType }) => mediaType))
369
300
 
370
- return notAcceptableResponse(url, supportedContentTypes)
301
+ return notAcceptableResponse(url, requestedMimeTypes, supportedContentTypes)
371
302
  }
372
303
 
373
304
  return acceptable
@@ -526,3 +457,47 @@ export class VerifiedFetch {
526
457
  await this.helia.stop()
527
458
  }
528
459
  }
460
+
461
+ export interface RequestedMimeType {
462
+ mediaType: string
463
+ options: Record<string, string>
464
+ }
465
+
466
+ function getRequestedMimeTypes (url: URL, accept?: string | null): RequestedMimeType[] {
467
+ if (accept == null || accept === '') {
468
+ // yolo content-type
469
+ accept = '*/*'
470
+ }
471
+
472
+ return accept
473
+ .split(',')
474
+ .map(s => {
475
+ const parts = s.trim().split(';')
476
+
477
+ const options: Record<string, string> = {
478
+ q: '1'
479
+ }
480
+
481
+ for (let i = 1; i < parts.length; i++) {
482
+ const [key, value] = parts[i].split('=').map(s => s.trim())
483
+
484
+ options[key] = value
485
+ }
486
+
487
+ return {
488
+ mediaType: `${parts[0]}`.trim(),
489
+ options
490
+ }
491
+ })
492
+ .sort((a, b) => {
493
+ if (a.options.q === b.options.q) {
494
+ return 0
495
+ }
496
+
497
+ if (a.options.q > b.options.q) {
498
+ return -1
499
+ }
500
+
501
+ return 1
502
+ })
503
+ }