@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.
- package/dist/index.min.js +31 -31
- package/dist/index.min.js.map +3 -3
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +4 -4
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipld.js +2 -2
- package/dist/src/plugins/plugin-handle-ipld.js.map +1 -1
- package/dist/src/utils/error-to-response.d.ts.map +1 -1
- package/dist/src/utils/error-to-response.js +5 -9
- package/dist/src/utils/error-to-response.js.map +1 -1
- package/dist/src/utils/responses.d.ts +1 -1
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +2 -1
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +4 -0
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +45 -69
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/plugins/plugin-handle-car.ts +4 -4
- package/src/plugins/plugin-handle-ipld.ts +2 -2
- package/src/utils/error-to-response.ts +5 -9
- package/src/utils/responses.ts +2 -1
- package/src/verified-fetch.ts +59 -84
|
@@ -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'
|
|
71
|
-
const duplicates = acceptCar.options.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 {
|
|
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
|
|
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
|
|
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(
|
|
44
|
-
return
|
|
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
|
package/src/utils/responses.ts
CHANGED
|
@@ -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
|
|
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 ?? {}),
|
package/src/verified-fetch.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
+
}
|