@helia/verified-fetch 2.3.1 → 2.5.0
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/README.md +200 -0
- package/dist/index.min.js +357 -35
- package/dist/src/index.d.ts +220 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +200 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/errors.d.ts +25 -0
- package/dist/src/plugins/errors.d.ts.map +1 -0
- package/dist/src/plugins/errors.js +33 -0
- package/dist/src/plugins/errors.js.map +1 -0
- package/dist/src/plugins/index.d.ts +8 -0
- package/dist/src/plugins/index.d.ts.map +1 -0
- package/dist/src/plugins/index.js +7 -0
- package/dist/src/plugins/index.js.map +1 -0
- package/dist/src/plugins/plugin-base.d.ts +19 -0
- package/dist/src/plugins/plugin-base.d.ts.map +1 -0
- package/dist/src/plugins/plugin-base.js +26 -0
- package/dist/src/plugins/plugin-base.js.map +1 -0
- package/dist/src/plugins/plugin-handle-car.d.ts +11 -0
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-car.js +28 -0
- package/dist/src/plugins/plugin-handle-car.js.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts +11 -0
- package/dist/src/plugins/plugin-handle-dag-cbor.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-cbor.js +73 -0
- package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts +15 -0
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-pb.js +152 -0
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts +16 -0
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-dag-walk.js +45 -0
- package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -0
- package/dist/src/plugins/plugin-handle-dir-index-html.d.ts +9 -0
- package/dist/src/plugins/plugin-handle-dir-index-html.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-dir-index-html.js +37 -0
- package/dist/src/plugins/plugin-handle-dir-index-html.js.map +1 -0
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +12 -0
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-ipns-record.js +62 -0
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -0
- package/dist/src/plugins/plugin-handle-json.d.ts +11 -0
- package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-json.js +51 -0
- package/dist/src/plugins/plugin-handle-json.js.map +1 -0
- package/dist/src/plugins/plugin-handle-raw.d.ts +8 -0
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-raw.js +80 -0
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -0
- package/dist/src/plugins/plugin-handle-tar.d.ts +12 -0
- package/dist/src/plugins/plugin-handle-tar.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-tar.js +36 -0
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -0
- package/dist/src/plugins/plugins.d.ts +5 -0
- package/dist/src/plugins/plugins.d.ts.map +1 -0
- package/dist/src/plugins/plugins.js +5 -0
- package/dist/src/plugins/plugins.js.map +1 -0
- package/dist/src/plugins/types.d.ts +68 -0
- package/dist/src/plugins/types.d.ts.map +1 -0
- package/dist/src/plugins/types.js +2 -0
- package/dist/src/plugins/types.js.map +1 -0
- package/dist/src/types.d.ts +0 -23
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -2
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/dir-index-html.d.ts +16 -0
- package/dist/src/utils/dir-index-html.d.ts.map +1 -0
- package/dist/src/utils/dir-index-html.js +387 -0
- package/dist/src/utils/dir-index-html.js.map +1 -0
- package/dist/src/utils/get-e-tag.d.ts +1 -1
- package/dist/src/utils/get-e-tag.d.ts.map +1 -1
- package/dist/src/utils/get-e-tag.js +18 -3
- package/dist/src/utils/get-e-tag.js.map +1 -1
- package/dist/src/utils/parse-resource.d.ts +2 -1
- package/dist/src/utils/parse-resource.d.ts.map +1 -1
- package/dist/src/utils/parse-resource.js +4 -3
- package/dist/src/utils/parse-resource.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +8 -3
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +30 -4
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/server-timing.d.ts +13 -0
- package/dist/src/utils/server-timing.d.ts.map +1 -0
- package/dist/src/utils/server-timing.js +19 -0
- package/dist/src/utils/server-timing.js.map +1 -0
- package/dist/src/utils/walk-path.d.ts +3 -2
- package/dist/src/utils/walk-path.d.ts.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +11 -20
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +174 -367
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +32 -24
- package/package.json +6 -2
- package/src/index.ts +223 -0
- package/src/plugins/errors.ts +37 -0
- package/src/plugins/index.ts +8 -0
- package/src/plugins/plugin-base.ts +30 -0
- package/src/plugins/plugin-handle-car.ts +32 -0
- package/src/plugins/plugin-handle-dag-cbor.ts +84 -0
- package/src/plugins/plugin-handle-dag-pb.ts +168 -0
- package/src/plugins/plugin-handle-dag-walk.ts +53 -0
- package/src/plugins/plugin-handle-dir-index-html.ts +44 -0
- package/src/plugins/plugin-handle-ipns-record.ts +69 -0
- package/src/plugins/plugin-handle-json.ts +57 -0
- package/src/plugins/plugin-handle-raw.ts +92 -0
- package/src/plugins/plugin-handle-tar.ts +44 -0
- package/src/plugins/plugins.ts +4 -0
- package/src/plugins/types.ts +73 -0
- package/src/types.ts +0 -29
- package/src/utils/dir-index-html.ts +445 -0
- package/src/utils/get-e-tag.ts +20 -3
- package/src/utils/parse-resource.ts +5 -4
- package/src/utils/parse-url-string.ts +38 -7
- package/src/utils/server-timing.ts +37 -0
- package/src/utils/walk-path.ts +3 -3
- package/src/verified-fetch.ts +198 -403
package/src/verified-fetch.ts
CHANGED
|
@@ -1,41 +1,31 @@
|
|
|
1
|
-
import { car } from '@helia/car'
|
|
2
1
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
6
|
-
import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface'
|
|
7
|
-
import { Record as DHTRecord } from '@libp2p/kad-dht'
|
|
8
|
-
import { Key } from 'interface-datastore'
|
|
9
|
-
import { exporter, type ObjectNode } from 'ipfs-unixfs-exporter'
|
|
10
|
-
import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
2
|
+
import { type AbortOptions, type Logger } from '@libp2p/interface'
|
|
3
|
+
import { prefixLogger } from '@libp2p/logger'
|
|
11
4
|
import { LRUCache } from 'lru-cache'
|
|
12
5
|
import { type CID } from 'multiformats/cid'
|
|
13
|
-
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
14
|
-
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
15
|
-
import { identity } from 'multiformats/hashes/identity'
|
|
16
6
|
import { CustomProgressEvent } from 'progress-events'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
7
|
+
import { CarPlugin } from './plugins/plugin-handle-car.js'
|
|
8
|
+
import { DagCborPlugin } from './plugins/plugin-handle-dag-cbor.js'
|
|
9
|
+
import { DagPbPlugin } from './plugins/plugin-handle-dag-pb.js'
|
|
10
|
+
import { DagWalkPlugin } from './plugins/plugin-handle-dag-walk.js'
|
|
11
|
+
import { IpnsRecordPlugin } from './plugins/plugin-handle-ipns-record.js'
|
|
12
|
+
import { JsonPlugin } from './plugins/plugin-handle-json.js'
|
|
13
|
+
import { RawPlugin } from './plugins/plugin-handle-raw.js'
|
|
14
|
+
import { TarPlugin } from './plugins/plugin-handle-tar.js'
|
|
22
15
|
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
23
16
|
import { getETag } from './utils/get-e-tag.js'
|
|
24
|
-
import { getPeerIdFromString } from './utils/get-peer-id-from-string.js'
|
|
25
17
|
import { getResolvedAcceptHeader } from './utils/get-resolved-accept-header.js'
|
|
26
|
-
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
27
|
-
import { tarStream } from './utils/get-tar-stream.js'
|
|
28
18
|
import { getRedirectResponse } from './utils/handle-redirects.js'
|
|
29
19
|
import { parseResource } from './utils/parse-resource.js'
|
|
30
20
|
import { type ParsedUrlStringResults } from './utils/parse-url-string.js'
|
|
31
21
|
import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
|
|
32
|
-
import { setCacheControlHeader
|
|
33
|
-
import { badRequestResponse,
|
|
22
|
+
import { setCacheControlHeader } from './utils/response-headers.js'
|
|
23
|
+
import { badRequestResponse, notAcceptableResponse, notSupportedResponse, badGatewayResponse } from './utils/responses.js'
|
|
34
24
|
import { selectOutputType } from './utils/select-output-type.js'
|
|
35
|
-
import {
|
|
36
|
-
import { handlePathWalking, isObjectNode } from './utils/walk-path.js'
|
|
25
|
+
import { serverTiming } from './utils/server-timing.js'
|
|
37
26
|
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
38
|
-
import type {
|
|
27
|
+
import type { VerifiedFetchPlugin, PluginContext, PluginOptions } from './plugins/types.js'
|
|
28
|
+
import type { RequestFormatShorthand } from './types.js'
|
|
39
29
|
import type { Helia, SessionBlockstore } from '@helia/interface'
|
|
40
30
|
import type { Blockstore } from 'interface-blockstore'
|
|
41
31
|
|
|
@@ -47,10 +37,6 @@ interface VerifiedFetchComponents {
|
|
|
47
37
|
ipns?: IPNS
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
interface FetchHandlerFunction {
|
|
51
|
-
(options: FetchHandlerFunctionArg): Promise<Response>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
40
|
function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOptions, 'signal'> & AbortOptions) | undefined {
|
|
55
41
|
if (options == null) {
|
|
56
42
|
return undefined
|
|
@@ -69,40 +55,14 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
69
55
|
}
|
|
70
56
|
}
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* if the user has specified an `Accept` header, and it's in our list of
|
|
84
|
-
* allowable "raw" format headers, use that instead of detecting the content
|
|
85
|
-
* type. This avoids the user from receiving something different when they
|
|
86
|
-
* signal that they want to `Accept` a specific mime type.
|
|
87
|
-
*/
|
|
88
|
-
function getOverridenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?: string }): string | undefined {
|
|
89
|
-
// accept has already been resolved by getResolvedAcceptHeader, if we have it, use it.
|
|
90
|
-
const acceptHeader = accept ?? new Headers(headers).get('accept') ?? ''
|
|
91
|
-
|
|
92
|
-
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
93
|
-
const acceptHeaders = acceptHeader.split(',')
|
|
94
|
-
.map(s => s.split(';')[0])
|
|
95
|
-
.map(s => s.trim())
|
|
96
|
-
|
|
97
|
-
for (const mimeType of acceptHeaders) {
|
|
98
|
-
if (mimeType === '*/*') {
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (RAW_HEADERS.includes(mimeType ?? '')) {
|
|
103
|
-
return mimeType
|
|
104
|
-
}
|
|
105
|
-
}
|
|
58
|
+
// TODO: merge/combine with PluginContext
|
|
59
|
+
interface FinalResponseContext {
|
|
60
|
+
cid?: ParsedUrlStringResults['cid']
|
|
61
|
+
reqFormat?: RequestFormatShorthand
|
|
62
|
+
ttl?: ParsedUrlStringResults['ttl']
|
|
63
|
+
protocol?: ParsedUrlStringResults['protocol']
|
|
64
|
+
ipfsPath?: string
|
|
65
|
+
query?: ParsedUrlStringResults['query']
|
|
106
66
|
}
|
|
107
67
|
|
|
108
68
|
export class VerifiedFetch {
|
|
@@ -111,6 +71,9 @@ export class VerifiedFetch {
|
|
|
111
71
|
private readonly log: Logger
|
|
112
72
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
113
73
|
private readonly blockstoreSessions: LRUCache<string, SessionBlockstore>
|
|
74
|
+
private serverTimingHeaders: string[] = []
|
|
75
|
+
private readonly withServerTiming: boolean
|
|
76
|
+
private readonly plugins: VerifiedFetchPlugin[] = []
|
|
114
77
|
|
|
115
78
|
constructor ({ helia, ipns }: VerifiedFetchComponents, init?: CreateVerifiedFetchOptions) {
|
|
116
79
|
this.helia = helia
|
|
@@ -124,10 +87,47 @@ export class VerifiedFetch {
|
|
|
124
87
|
store.close()
|
|
125
88
|
}
|
|
126
89
|
})
|
|
90
|
+
this.withServerTiming = init?.withServerTiming ?? false
|
|
91
|
+
|
|
92
|
+
const pluginOptions: PluginOptions = {
|
|
93
|
+
...init,
|
|
94
|
+
logger: prefixLogger('helia:verified-fetch'),
|
|
95
|
+
getBlockstore: (cid, resource, useSession, options) => this.getBlockstore(cid, resource, useSession, options),
|
|
96
|
+
handleServerTiming: async (name, description, fn) => this.handleServerTiming(name, description, fn, this.withServerTiming),
|
|
97
|
+
helia,
|
|
98
|
+
contentTypeParser: this.contentTypeParser
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const defaultPlugins = [
|
|
102
|
+
new DagWalkPlugin(pluginOptions),
|
|
103
|
+
new IpnsRecordPlugin(pluginOptions),
|
|
104
|
+
new CarPlugin(pluginOptions),
|
|
105
|
+
new RawPlugin(pluginOptions),
|
|
106
|
+
new TarPlugin(pluginOptions),
|
|
107
|
+
new JsonPlugin(pluginOptions),
|
|
108
|
+
new DagCborPlugin(pluginOptions),
|
|
109
|
+
new DagPbPlugin(pluginOptions)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const customPlugins = init?.plugins?.map((pluginFactory) => pluginFactory(pluginOptions)) ?? []
|
|
113
|
+
|
|
114
|
+
if (customPlugins.length > 0) {
|
|
115
|
+
// allow custom plugins to replace default plugins
|
|
116
|
+
const defaultPluginMap = new Map(defaultPlugins.map(plugin => [plugin.constructor.name, plugin]))
|
|
117
|
+
const customPluginMap = new Map(customPlugins.map(plugin => [plugin.constructor.name, plugin]))
|
|
118
|
+
|
|
119
|
+
this.plugins = defaultPlugins.map(plugin => customPluginMap.get(plugin.constructor.name) ?? plugin)
|
|
120
|
+
|
|
121
|
+
// Add any remaining custom plugins that don't replace a default plugin
|
|
122
|
+
this.plugins.push(...customPlugins.filter(plugin => !defaultPluginMap.has(plugin.constructor.name)))
|
|
123
|
+
} else {
|
|
124
|
+
this.plugins = defaultPlugins
|
|
125
|
+
}
|
|
126
|
+
|
|
127
127
|
this.log.trace('created VerifiedFetch instance')
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
private getBlockstore (root: CID, resource: string | CID, useSession: boolean, options
|
|
130
|
+
private getBlockstore (root: CID, resource: string | CID, useSession: boolean = true, options: AbortOptions = {}): Blockstore {
|
|
131
131
|
const key = resourceToSessionCacheKey(resource)
|
|
132
132
|
if (!useSession) {
|
|
133
133
|
return this.helia.blockstore
|
|
@@ -143,313 +143,154 @@ export class VerifiedFetch {
|
|
|
143
143
|
return session
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*/
|
|
150
|
-
private async handleIPNSRecord ({ resource, cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
151
|
-
if (path !== '' || !(resource.startsWith('ipns://') || resource.includes('.ipns.'))) {
|
|
152
|
-
this.log.error('invalid request for IPNS name "%s" and path "%s"', resource, path)
|
|
153
|
-
return badRequestResponse(resource, 'Invalid IPNS name')
|
|
146
|
+
private async handleServerTiming<T> (name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T> {
|
|
147
|
+
if (!withServerTiming) {
|
|
148
|
+
return fn()
|
|
154
149
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (resource.startsWith('ipns://')) {
|
|
160
|
-
const peerIdString = resource.replace('ipns://', '')
|
|
161
|
-
this.log.trace('trying to parse peer id from "%s"', peerIdString)
|
|
162
|
-
peerId = getPeerIdFromString(peerIdString)
|
|
163
|
-
} else {
|
|
164
|
-
const peerIdString = resource.split('.ipns.')[0].split('://')[1]
|
|
165
|
-
this.log.trace('trying to parse peer id from "%s"', peerIdString)
|
|
166
|
-
peerId = getPeerIdFromString(peerIdString)
|
|
167
|
-
}
|
|
168
|
-
} catch (err: any) {
|
|
169
|
-
this.log.error('could not parse peer id from IPNS url %s', resource, err)
|
|
170
|
-
|
|
171
|
-
return badRequestResponse(resource, err)
|
|
150
|
+
const { error, result, header } = await serverTiming(name, description, fn)
|
|
151
|
+
this.serverTimingHeaders.push(header)
|
|
152
|
+
if (error != null) {
|
|
153
|
+
throw error
|
|
172
154
|
}
|
|
173
155
|
|
|
174
|
-
|
|
175
|
-
// IPNS name so a local copy should be in the helia datastore, so we can
|
|
176
|
-
// just read it out..
|
|
177
|
-
const routingKey = uint8ArrayConcat([
|
|
178
|
-
uint8ArrayFromString('/ipns/'),
|
|
179
|
-
peerId.toMultihash().bytes
|
|
180
|
-
])
|
|
181
|
-
const datastoreKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false)
|
|
182
|
-
const buf = await this.helia.datastore.get(datastoreKey, options)
|
|
183
|
-
const record = DHTRecord.deserialize(buf)
|
|
184
|
-
|
|
185
|
-
const response = okResponse(resource, record.value)
|
|
186
|
-
response.headers.set('content-type', 'application/vnd.ipfs.ipns-record')
|
|
187
|
-
|
|
188
|
-
return response
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Accepts a `CID` and returns a `Response` with a body stream that is a CAR
|
|
193
|
-
* of the `DAG` referenced by the `CID`.
|
|
194
|
-
*/
|
|
195
|
-
private async handleCar ({ resource, cid, session, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
196
|
-
const blockstore = this.getBlockstore(cid, resource, session, options)
|
|
197
|
-
const c = car({ blockstore, getCodec: this.helia.getCodec })
|
|
198
|
-
const stream = toBrowserReadableStream(c.stream(cid, options))
|
|
199
|
-
|
|
200
|
-
const response = okResponse(resource, stream)
|
|
201
|
-
response.headers.set('content-type', 'application/vnd.ipld.car; version=1')
|
|
202
|
-
|
|
203
|
-
return response
|
|
156
|
+
return result
|
|
204
157
|
}
|
|
205
158
|
|
|
206
159
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
160
|
+
* The last place a Response touches in verified-fetch before being returned to the user. This is where we add the
|
|
161
|
+
* Server-Timing header to the response if it has been collected. It should be used for any final processing of the
|
|
162
|
+
* response before it is returned to the user.
|
|
209
163
|
*/
|
|
210
|
-
private
|
|
211
|
-
if (
|
|
212
|
-
|
|
164
|
+
private handleFinalResponse (response: Response, { query, cid, reqFormat, ttl, protocol, ipfsPath }: FinalResponseContext = {}): Response {
|
|
165
|
+
if (this.serverTimingHeaders.length > 0) {
|
|
166
|
+
const headerString = this.serverTimingHeaders.join(', ')
|
|
167
|
+
response.headers.set('Server-Timing', headerString)
|
|
168
|
+
this.serverTimingHeaders = []
|
|
213
169
|
}
|
|
214
170
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const response = okResponse(resource, stream)
|
|
219
|
-
response.headers.set('content-type', 'application/x-tar')
|
|
171
|
+
// set Content-Disposition header
|
|
172
|
+
let contentDisposition: string | undefined
|
|
220
173
|
|
|
221
|
-
|
|
222
|
-
}
|
|
174
|
+
this.log.trace('checking for content disposition')
|
|
223
175
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const block = await blockstore.get(cid, options)
|
|
228
|
-
let body: string | Uint8Array
|
|
229
|
-
|
|
230
|
-
if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
231
|
-
try {
|
|
232
|
-
// if vnd.ipld.dag-cbor has been specified, convert to the format - note
|
|
233
|
-
// that this supports more data types than regular JSON, the content-type
|
|
234
|
-
// response header is set so the user knows to process it differently
|
|
235
|
-
const obj = ipldDagJson.decode(block)
|
|
236
|
-
body = ipldDagCbor.encode(obj)
|
|
237
|
-
} catch (err) {
|
|
238
|
-
this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
|
|
239
|
-
return notAcceptableResponse(resource)
|
|
240
|
-
}
|
|
176
|
+
// force download if requested
|
|
177
|
+
if (query?.download === true) {
|
|
178
|
+
contentDisposition = 'attachment'
|
|
241
179
|
} else {
|
|
242
|
-
|
|
243
|
-
body = block
|
|
180
|
+
this.log.trace('download not requested')
|
|
244
181
|
}
|
|
245
182
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
private async handleDagCbor ({ resource, cid, path, accept, session, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
252
|
-
this.log.trace('fetching %c/%s', cid, path)
|
|
253
|
-
let terminalElement: ObjectNode
|
|
254
|
-
const blockstore = this.getBlockstore(cid, resource, session, options)
|
|
183
|
+
// override filename if requested
|
|
184
|
+
if (query?.filename != null) {
|
|
185
|
+
if (contentDisposition == null) {
|
|
186
|
+
contentDisposition = 'inline'
|
|
187
|
+
}
|
|
255
188
|
|
|
256
|
-
|
|
257
|
-
const pathDetails = await handlePathWalking({ cid, path, resource, options, blockstore, log: this.log })
|
|
258
|
-
if (pathDetails instanceof Response) {
|
|
259
|
-
return pathDetails
|
|
260
|
-
}
|
|
261
|
-
const ipfsRoots = pathDetails.ipfsRoots
|
|
262
|
-
if (isObjectNode(pathDetails.terminalElement)) {
|
|
263
|
-
terminalElement = pathDetails.terminalElement
|
|
189
|
+
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
264
190
|
} else {
|
|
265
|
-
|
|
266
|
-
this.log.error('terminal element is not a dag-cbor node')
|
|
267
|
-
return notSupportedResponse(resource, 'Terminal element is not a dag-cbor node')
|
|
191
|
+
this.log.trace('no filename specified in query')
|
|
268
192
|
}
|
|
269
193
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
let body: string | Uint8Array
|
|
273
|
-
|
|
274
|
-
if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
275
|
-
// skip decoding
|
|
276
|
-
body = block
|
|
277
|
-
} else if (accept === 'application/vnd.ipld.dag-json') {
|
|
278
|
-
try {
|
|
279
|
-
// if vnd.ipld.dag-json has been specified, convert to the format - note
|
|
280
|
-
// that this supports more data types than regular JSON, the content-type
|
|
281
|
-
// response header is set so the user knows to process it differently
|
|
282
|
-
const obj = ipldDagCbor.decode(block)
|
|
283
|
-
body = ipldDagJson.encode(obj)
|
|
284
|
-
} catch (err) {
|
|
285
|
-
this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
|
|
286
|
-
return notAcceptableResponse(resource)
|
|
287
|
-
}
|
|
194
|
+
if (contentDisposition != null) {
|
|
195
|
+
response.headers.set('Content-Disposition', contentDisposition)
|
|
288
196
|
} else {
|
|
289
|
-
|
|
290
|
-
body = dagCborToSafeJSON(block)
|
|
291
|
-
} catch (err) {
|
|
292
|
-
if (accept === 'application/json') {
|
|
293
|
-
this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
|
|
294
|
-
|
|
295
|
-
return notAcceptableResponse(resource)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
299
|
-
body = block
|
|
300
|
-
}
|
|
197
|
+
this.log.trace('no content disposition specified')
|
|
301
198
|
}
|
|
302
199
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (accept == null) {
|
|
306
|
-
accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
|
|
200
|
+
if (cid != null && response.headers.get('etag') == null) {
|
|
201
|
+
response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
|
|
307
202
|
}
|
|
308
203
|
|
|
309
|
-
|
|
310
|
-
|
|
204
|
+
if (protocol != null) {
|
|
205
|
+
setCacheControlHeader({ response, ttl, protocol })
|
|
206
|
+
}
|
|
207
|
+
if (ipfsPath != null) {
|
|
208
|
+
response.headers.set('X-Ipfs-Path', ipfsPath)
|
|
209
|
+
}
|
|
311
210
|
|
|
312
211
|
return response
|
|
313
212
|
}
|
|
314
213
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Runs plugins in a loop. After each plugin that returns `null` (partial/no final),
|
|
216
|
+
* we re-check `canHandle()` for all plugins in the next iteration if the context changed.
|
|
217
|
+
*/
|
|
218
|
+
private async runPluginPipeline (context: PluginContext, maxPasses: number = 3): Promise<Response | undefined> {
|
|
219
|
+
let finalResponse: Response | undefined
|
|
220
|
+
let passCount = 0
|
|
221
|
+
const pluginsUsed = new Set<string>()
|
|
222
|
+
|
|
223
|
+
let prevModificationId = context.modified
|
|
224
|
+
|
|
225
|
+
while (passCount < maxPasses) {
|
|
226
|
+
this.log(`Starting pipeline pass #${passCount + 1}`)
|
|
227
|
+
passCount++
|
|
228
|
+
|
|
229
|
+
// gather plugins that say they can handle the *current* context, but haven't been used yet
|
|
230
|
+
const readyPlugins = this.plugins.filter(p => !pluginsUsed.has(p.constructor.name)).filter(p => p.canHandle(context))
|
|
231
|
+
if (readyPlugins.length === 0) {
|
|
232
|
+
this.log.trace('No plugins can handle the current context.. checking by CID code')
|
|
233
|
+
const plugins = this.plugins.filter(p => p.codes.includes(context.cid.code))
|
|
234
|
+
if (plugins.length > 0) {
|
|
235
|
+
readyPlugins.push(...plugins)
|
|
236
|
+
} else {
|
|
237
|
+
this.log.trace('No plugins found that can handle request by CID code; exiting pipeline.')
|
|
238
|
+
break
|
|
339
239
|
}
|
|
340
|
-
|
|
341
|
-
// fall-through simulates following the redirect?
|
|
342
|
-
resource = `${resource}/`
|
|
343
|
-
redirected = true
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const rootFilePath = 'index.html'
|
|
347
|
-
try {
|
|
348
|
-
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
349
|
-
|
|
350
|
-
const entry = await exporter(`/ipfs/${dirCid}/${rootFilePath}`, this.helia.blockstore, {
|
|
351
|
-
signal: options?.signal,
|
|
352
|
-
onProgress: options?.onProgress
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
this.log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
|
|
356
|
-
path = rootFilePath
|
|
357
|
-
resolvedCID = entry.cid
|
|
358
|
-
} catch (err: any) {
|
|
359
|
-
options?.signal?.throwIfAborted()
|
|
360
|
-
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
361
|
-
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
362
|
-
} finally {
|
|
363
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
|
|
364
240
|
}
|
|
365
|
-
}
|
|
366
241
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
242
|
+
this.log.trace('Plugins ready to handle request: ', readyPlugins.map(p => p.constructor.name).join(', '))
|
|
243
|
+
|
|
244
|
+
// track if any plugin changed the context or returned a response
|
|
245
|
+
let contextChanged = false
|
|
246
|
+
let pluginHandled = false
|
|
247
|
+
|
|
248
|
+
for (const plugin of readyPlugins) {
|
|
249
|
+
try {
|
|
250
|
+
this.log.trace('Invoking plugin:', plugin.constructor.name)
|
|
251
|
+
pluginsUsed.add(plugin.constructor.name)
|
|
252
|
+
|
|
253
|
+
const maybeResponse = await plugin.handle(context)
|
|
254
|
+
if (maybeResponse != null) {
|
|
255
|
+
// if a plugin returns a final Response, short-circuit
|
|
256
|
+
finalResponse = maybeResponse
|
|
257
|
+
pluginHandled = true
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
} catch (err: any) {
|
|
261
|
+
context.options?.signal?.throwIfAborted()
|
|
262
|
+
this.log.error('Error in plugin:', plugin.constructor.name, err)
|
|
263
|
+
// if fatal, short-circuit the pipeline
|
|
264
|
+
if (err.name === 'PluginFatalError') {
|
|
265
|
+
// if plugin provides a custom error response, return it
|
|
266
|
+
return err.response ?? badGatewayResponse(context.resource, 'Failed to fetch')
|
|
267
|
+
}
|
|
268
|
+
} finally {
|
|
269
|
+
// on each plugin call, check for changes in the context
|
|
270
|
+
const newModificationId = context.modified
|
|
271
|
+
contextChanged = newModificationId !== prevModificationId
|
|
272
|
+
if (contextChanged) {
|
|
273
|
+
prevModificationId = newModificationId
|
|
274
|
+
}
|
|
275
|
+
}
|
|
370
276
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
277
|
+
if (finalResponse != null) {
|
|
278
|
+
this.log.trace('Plugin produced final response:', plugin.constructor.name)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
376
282
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
signal: options?.signal,
|
|
380
|
-
onProgress: options?.onProgress
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
const asyncIter = entry.content({
|
|
384
|
-
signal: options?.signal,
|
|
385
|
-
onProgress: options?.onProgress,
|
|
386
|
-
offset,
|
|
387
|
-
length
|
|
388
|
-
})
|
|
389
|
-
this.log('got async iterator for %c/%s', cid, path)
|
|
390
|
-
|
|
391
|
-
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
392
|
-
onProgress: options?.onProgress,
|
|
393
|
-
signal: options?.signal
|
|
394
|
-
})
|
|
395
|
-
byteRangeContext.setBody(stream)
|
|
396
|
-
// if not a valid range request, okRangeRequest will call okResponse
|
|
397
|
-
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
|
|
398
|
-
redirected
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
await setContentType({ bytes: firstChunk, path, response, contentTypeParser: this.contentTypeParser, log: this.log })
|
|
402
|
-
setIpfsRoots(response, ipfsRoots)
|
|
403
|
-
|
|
404
|
-
return response
|
|
405
|
-
} catch (err: any) {
|
|
406
|
-
options?.signal?.throwIfAborted()
|
|
407
|
-
this.log.error('error streaming %c/%s', cid, path, err)
|
|
408
|
-
if (byteRangeContext.isRangeRequest && err.code === 'ERR_INVALID_PARAMS') {
|
|
409
|
-
return badRangeResponse(resource)
|
|
283
|
+
if (pluginHandled && finalResponse != null) {
|
|
284
|
+
break
|
|
410
285
|
}
|
|
411
|
-
return badGatewayResponse(resource.toString(), 'Unable to stream content')
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
286
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
* @see https://github.com/ipfs/gateway-conformance/blob/26994cfb056b717a23bf694ce4e94386728748dd/tests/subdomain_gateway_ipfs_test.go#L198-L204
|
|
420
|
-
*/
|
|
421
|
-
if (path !== '') {
|
|
422
|
-
this.log.trace('404-ing raw codec request for %c/%s', cid, path)
|
|
423
|
-
return notFoundResponse(resource, 'Raw codec does not support paths')
|
|
287
|
+
if (!contextChanged) {
|
|
288
|
+
this.log.trace('No context changes and no final response; exiting pipeline.')
|
|
289
|
+
break
|
|
290
|
+
}
|
|
424
291
|
}
|
|
425
292
|
|
|
426
|
-
|
|
427
|
-
const blockstore = this.getBlockstore(cid, resource, session, options)
|
|
428
|
-
const result = await blockstore.get(cid, options)
|
|
429
|
-
byteRangeContext.setBody(result)
|
|
430
|
-
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
|
|
431
|
-
redirected: false
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
// if the user has specified an `Accept` header that corresponds to a raw
|
|
435
|
-
// type, honour that header, so for example they don't request
|
|
436
|
-
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
437
|
-
await setContentType({ bytes: result, path, response, defaultContentType: getOverridenRawContentType({ headers: options?.headers, accept }), contentTypeParser: this.contentTypeParser, log: this.log })
|
|
438
|
-
|
|
439
|
-
return response
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* If the user has not specified an Accept header or format query string arg,
|
|
444
|
-
* use the CID codec to choose an appropriate handler for the block data.
|
|
445
|
-
*/
|
|
446
|
-
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
447
|
-
[dagPbCode]: this.handleDagPb,
|
|
448
|
-
[ipldDagJson.code]: this.handleJson,
|
|
449
|
-
[jsonCode]: this.handleJson,
|
|
450
|
-
[ipldDagCbor.code]: this.handleDagCbor,
|
|
451
|
-
[rawCode]: this.handleRaw,
|
|
452
|
-
[identity.code]: this.handleRaw
|
|
293
|
+
return finalResponse
|
|
453
294
|
}
|
|
454
295
|
|
|
455
296
|
/**
|
|
@@ -463,6 +304,7 @@ export class VerifiedFetch {
|
|
|
463
304
|
this.log('fetch %s', resource)
|
|
464
305
|
|
|
465
306
|
const options = convertOptions(opts)
|
|
307
|
+
const withServerTiming = options?.withServerTiming ?? this.withServerTiming
|
|
466
308
|
|
|
467
309
|
options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
|
|
468
310
|
// resolve the CID/path from the requested resource
|
|
@@ -473,105 +315,58 @@ export class VerifiedFetch {
|
|
|
473
315
|
let protocol: ParsedUrlStringResults['protocol']
|
|
474
316
|
let ipfsPath: string
|
|
475
317
|
try {
|
|
476
|
-
const result = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
318
|
+
const result = await this.handleServerTiming('parse-resource', '', async () => parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, { withServerTiming, ...options }), withServerTiming)
|
|
477
319
|
cid = result.cid
|
|
478
320
|
path = result.path
|
|
479
321
|
query = result.query
|
|
480
322
|
ttl = result.ttl
|
|
481
323
|
protocol = result.protocol
|
|
482
324
|
ipfsPath = result.ipfsPath
|
|
325
|
+
this.serverTimingHeaders.push(...result.serverTimings.map(({ header }) => header))
|
|
483
326
|
} catch (err: any) {
|
|
484
327
|
options?.signal?.throwIfAborted()
|
|
485
328
|
this.log.error('error parsing resource %s', resource, err)
|
|
486
329
|
|
|
487
|
-
return badRequestResponse(resource.toString(), err)
|
|
330
|
+
return this.handleFinalResponse(badRequestResponse(resource.toString(), err))
|
|
488
331
|
}
|
|
489
332
|
|
|
490
333
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
491
334
|
|
|
492
335
|
const acceptHeader = getResolvedAcceptHeader({ query, headers: options?.headers, logger: this.helia.logger })
|
|
493
336
|
|
|
494
|
-
const accept = selectOutputType(cid, acceptHeader)
|
|
337
|
+
const accept: string | undefined = selectOutputType(cid, acceptHeader)
|
|
495
338
|
this.log('output type %s', accept)
|
|
496
339
|
|
|
497
340
|
if (acceptHeader != null && accept == null) {
|
|
498
|
-
return notAcceptableResponse(resource.toString())
|
|
341
|
+
return this.handleFinalResponse(notAcceptableResponse(resource.toString()))
|
|
499
342
|
}
|
|
500
343
|
|
|
501
|
-
|
|
502
|
-
let reqFormat: RequestFormatShorthand | undefined
|
|
344
|
+
const responseContentType: string = accept?.split(';')[0] ?? 'application/octet-stream'
|
|
503
345
|
|
|
504
346
|
const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid })
|
|
505
347
|
if (redirectResponse != null) {
|
|
506
|
-
return redirectResponse
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, session: options?.session ?? true, options }
|
|
510
|
-
|
|
511
|
-
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
512
|
-
// the user requested a raw IPNS record
|
|
513
|
-
reqFormat = 'ipns-record'
|
|
514
|
-
response = await this.handleIPNSRecord(handlerArgs)
|
|
515
|
-
} else if (accept === 'application/vnd.ipld.car') {
|
|
516
|
-
// the user requested a CAR file
|
|
517
|
-
reqFormat = 'car'
|
|
518
|
-
query.download = true
|
|
519
|
-
query.filename = query.filename ?? `${cid.toString()}.car`
|
|
520
|
-
response = await this.handleCar(handlerArgs)
|
|
521
|
-
} else if (accept === 'application/vnd.ipld.raw') {
|
|
522
|
-
// the user requested a raw block
|
|
523
|
-
reqFormat = 'raw'
|
|
524
|
-
query.download = true
|
|
525
|
-
query.filename = query.filename ?? `${cid.toString()}.bin`
|
|
526
|
-
response = await this.handleRaw(handlerArgs)
|
|
527
|
-
} else if (accept === 'application/x-tar') {
|
|
528
|
-
// the user requested a TAR file
|
|
529
|
-
reqFormat = 'tar'
|
|
530
|
-
query.download = true
|
|
531
|
-
query.filename = query.filename ?? `${cid.toString()}.tar`
|
|
532
|
-
response = await this.handleTar(handlerArgs)
|
|
533
|
-
} else {
|
|
534
|
-
this.log.trace('finding handler for cid code "%s" and output type "%s"', cid.code, accept)
|
|
535
|
-
// derive the handler from the CID type
|
|
536
|
-
const codecHandler = this.codecHandlers[cid.code]
|
|
537
|
-
|
|
538
|
-
if (codecHandler == null) {
|
|
539
|
-
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`)
|
|
540
|
-
}
|
|
541
|
-
this.log.trace('calling handler "%s"', codecHandler.name)
|
|
542
|
-
|
|
543
|
-
response = await codecHandler.call(this, handlerArgs)
|
|
348
|
+
return this.handleFinalResponse(redirectResponse)
|
|
544
349
|
}
|
|
545
350
|
|
|
546
|
-
|
|
351
|
+
const context: PluginContext = { cid, path, resource: resource.toString(), accept, query, options, withServerTiming, onProgress: options?.onProgress, modified: 0 }
|
|
547
352
|
|
|
548
|
-
|
|
549
|
-
response.headers.set('X-Ipfs-Path', ipfsPath)
|
|
353
|
+
this.log.trace('finding handler for cid code "%s" and response content type "%s"', cid.code, responseContentType)
|
|
550
354
|
|
|
551
|
-
|
|
552
|
-
let contentDisposition: string | undefined
|
|
553
|
-
|
|
554
|
-
// force download if requested
|
|
555
|
-
if (query.download === true) {
|
|
556
|
-
contentDisposition = 'attachment'
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// override filename if requested
|
|
560
|
-
if (query.filename != null) {
|
|
561
|
-
if (contentDisposition == null) {
|
|
562
|
-
contentDisposition = 'inline'
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (contentDisposition != null) {
|
|
569
|
-
response.headers.set('Content-Disposition', contentDisposition)
|
|
570
|
-
}
|
|
355
|
+
const response = await this.runPluginPipeline(context)
|
|
571
356
|
|
|
572
357
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
573
358
|
|
|
574
|
-
return response
|
|
359
|
+
return this.handleFinalResponse(response ?? notSupportedResponse(resource.toString()), {
|
|
360
|
+
query: {
|
|
361
|
+
...query,
|
|
362
|
+
...context.query
|
|
363
|
+
},
|
|
364
|
+
cid,
|
|
365
|
+
reqFormat: context.reqFormat,
|
|
366
|
+
ttl,
|
|
367
|
+
protocol,
|
|
368
|
+
ipfsPath
|
|
369
|
+
})
|
|
575
370
|
}
|
|
576
371
|
|
|
577
372
|
/**
|