@helia/verified-fetch 0.0.0-8db7792 → 0.0.0-a04e041

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 (33) hide show
  1. package/README.md +33 -0
  2. package/dist/index.min.js +4 -4
  3. package/dist/src/index.d.ts +36 -0
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +33 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/utils/get-content-disposition-filename.d.ts +6 -0
  8. package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -0
  9. package/dist/src/utils/get-content-disposition-filename.js +16 -0
  10. package/dist/src/utils/get-content-disposition-filename.js.map +1 -0
  11. package/dist/src/utils/parse-url-string.d.ts +2 -0
  12. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  13. package/dist/src/utils/parse-url-string.js +6 -0
  14. package/dist/src/utils/parse-url-string.js.map +1 -1
  15. package/dist/src/utils/responses.d.ts +4 -0
  16. package/dist/src/utils/responses.d.ts.map +1 -0
  17. package/dist/src/utils/responses.js +21 -0
  18. package/dist/src/utils/responses.js.map +1 -0
  19. package/dist/src/utils/select-output-type.d.ts +12 -0
  20. package/dist/src/utils/select-output-type.d.ts.map +1 -0
  21. package/dist/src/utils/select-output-type.js +147 -0
  22. package/dist/src/utils/select-output-type.js.map +1 -0
  23. package/dist/src/verified-fetch.d.ts +17 -15
  24. package/dist/src/verified-fetch.d.ts.map +1 -1
  25. package/dist/src/verified-fetch.js +211 -129
  26. package/dist/src/verified-fetch.js.map +1 -1
  27. package/package.json +18 -12
  28. package/src/index.ts +37 -0
  29. package/src/utils/get-content-disposition-filename.ts +18 -0
  30. package/src/utils/parse-url-string.ts +11 -1
  31. package/src/utils/responses.ts +22 -0
  32. package/src/utils/select-output-type.ts +166 -0
  33. package/src/verified-fetch.ts +237 -134
@@ -1,18 +1,23 @@
1
+ import { car } from '@helia/car'
1
2
  import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
2
3
  import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
3
4
  import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
4
- import { code as dagCborCode } from '@ipld/dag-cbor'
5
- import { code as dagJsonCode } from '@ipld/dag-json'
5
+ import * as ipldDagCbor from '@ipld/dag-cbor'
6
+ import * as ipldDagJson from '@ipld/dag-json'
6
7
  import { code as dagPbCode } from '@ipld/dag-pb'
8
+ import toBrowserReadableStream from 'it-to-browser-readablestream'
7
9
  import { code as jsonCode } from 'multiformats/codecs/json'
8
10
  import { code as rawCode } from 'multiformats/codecs/raw'
9
11
  import { identity } from 'multiformats/hashes/identity'
10
12
  import { CustomProgressEvent } from 'progress-events'
11
13
  import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
14
+ import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
12
15
  import { getETag } from './utils/get-e-tag.js'
13
16
  import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
14
17
  import { parseResource } from './utils/parse-resource.js'
15
- import { walkPath, type PathWalkerFn } from './utils/walk-path.js'
18
+ import { notAcceptableResponse, notSupportedResponse, okResponse } from './utils/responses.js'
19
+ import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
20
+ import { walkPath } from './utils/walk-path.js'
16
21
  import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
17
22
  import type { RequestFormatShorthand } from './types.js'
18
23
  import type { Helia } from '@helia/interface'
@@ -24,7 +29,6 @@ interface VerifiedFetchComponents {
24
29
  helia: Helia
25
30
  ipns?: IPNS
26
31
  unixfs?: HeliaUnixFs
27
- pathWalker?: PathWalkerFn
28
32
  }
29
33
 
30
34
  /**
@@ -37,8 +41,13 @@ interface VerifiedFetchInit {
37
41
  interface FetchHandlerFunctionArg {
38
42
  cid: CID
39
43
  path: string
40
- terminalElement?: UnixFSEntry
41
44
  options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
45
+
46
+ /**
47
+ * If present, the user has sent an accept header with this value - if the
48
+ * content cannot be represented in this format a 406 should be returned
49
+ */
50
+ accept?: string
42
51
  }
43
52
 
44
53
  interface FetchHandlerFunction {
@@ -60,29 +69,48 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
60
69
  }
61
70
  }
62
71
 
63
- function okResponse (body?: BodyInit | null): Response {
64
- return new Response(body, {
65
- status: 200,
66
- statusText: 'OK'
67
- })
68
- }
72
+ /**
73
+ * These are Accept header values that will cause content type sniffing to be
74
+ * skipped and set to these values.
75
+ */
76
+ const RAW_HEADERS = [
77
+ 'application/vnd.ipld.raw',
78
+ 'application/octet-stream'
79
+ ]
69
80
 
70
- function notSupportedResponse (body?: BodyInit | null): Response {
71
- return new Response(body, {
72
- status: 501,
73
- statusText: 'Not Implemented'
74
- })
81
+ /**
82
+ * if the user has specified an `Accept` header, and it's in our list of
83
+ * allowable "raw" format headers, use that instead of detecting the content
84
+ * type. This avoids the user from receiving something different when they
85
+ * signal that they want to `Accept` a specific mime type.
86
+ */
87
+ function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
88
+ const acceptHeader = new Headers(headers).get('accept') ?? ''
89
+
90
+ // e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
91
+ const acceptHeaders = acceptHeader.split(',')
92
+ .map(s => s.split(';')[0])
93
+ .map(s => s.trim())
94
+
95
+ for (const mimeType of acceptHeaders) {
96
+ if (mimeType === '*/*') {
97
+ return
98
+ }
99
+
100
+ if (RAW_HEADERS.includes(mimeType ?? '')) {
101
+ return mimeType
102
+ }
103
+ }
75
104
  }
76
105
 
77
106
  export class VerifiedFetch {
78
107
  private readonly helia: Helia
79
108
  private readonly ipns: IPNS
80
109
  private readonly unixfs: HeliaUnixFs
81
- private readonly pathWalker: PathWalkerFn
82
110
  private readonly log: Logger
83
111
  private readonly contentTypeParser: ContentTypeParser | undefined
84
112
 
85
- constructor ({ helia, ipns, unixfs, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
113
+ constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
86
114
  this.helia = helia
87
115
  this.log = helia.logger.forComponent('helia:verified-fetch')
88
116
  this.ipns = ipns ?? heliaIpns(helia, {
@@ -92,60 +120,128 @@ export class VerifiedFetch {
92
120
  ]
93
121
  })
94
122
  this.unixfs = unixfs ?? heliaUnixFs(helia)
95
- this.pathWalker = pathWalker ?? walkPath
96
123
  this.contentTypeParser = init?.contentTypeParser
97
124
  this.log.trace('created VerifiedFetch instance')
98
125
  }
99
126
 
100
- // handle vnd.ipfs.ipns-record
101
- private async handleIPNSRecord ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
102
- const response = notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
103
- response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
104
- return response
127
+ /**
128
+ * Accepts an `ipns://...` URL as a string and returns a `Response` containing
129
+ * a raw IPNS record.
130
+ */
131
+ private async handleIPNSRecord (resource: string, opts?: VerifiedFetchOptions): Promise<Response> {
132
+ return notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
105
133
  }
106
134
 
107
- // handle vnd.ipld.car
108
- private async handleIPLDCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
109
- const response = notSupportedResponse('vnd.ipld.car support is not implemented')
110
- response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
135
+ /**
136
+ * Accepts a `CID` and returns a `Response` with a body stream that is a CAR
137
+ * of the `DAG` referenced by the `CID`.
138
+ */
139
+ private async handleCar ({ cid, options }: FetchHandlerFunctionArg): Promise<Response> {
140
+ const c = car(this.helia)
141
+ const stream = toBrowserReadableStream(c.stream(cid, options))
142
+
143
+ const response = okResponse(stream)
144
+ response.headers.set('content-type', 'application/vnd.ipld.car; version=1')
145
+
111
146
  return response
112
147
  }
113
148
 
114
- private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
149
+ /**
150
+ * Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
151
+ * directory structure referenced by the `CID`.
152
+ */
153
+ private async handleTar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
154
+ if (cid.code !== dagPbCode) {
155
+ return notAcceptableResponse('only dag-pb CIDs can be returned in TAR files')
156
+ }
157
+
158
+ return notSupportedResponse('application/tar support is not implemented')
159
+ }
160
+
161
+ private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
115
162
  this.log.trace('fetching %c/%s', cid, path)
116
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
117
- const result = await this.helia.blockstore.get(cid, {
118
- signal: options?.signal,
119
- onProgress: options?.onProgress
120
- })
121
- const response = okResponse(result)
122
- response.headers.set('content-type', 'application/json')
123
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
163
+ const block = await this.helia.blockstore.get(cid, options)
164
+ let body: string | Uint8Array
165
+
166
+ if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
167
+ try {
168
+ // if vnd.ipld.dag-cbor has been specified, convert to the format - note
169
+ // that this supports more data types than regular JSON, the content-type
170
+ // response header is set so the user knows to process it differently
171
+ const obj = ipldDagJson.decode(block)
172
+ body = ipldDagCbor.encode(obj)
173
+ } catch (err) {
174
+ this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
175
+ return notAcceptableResponse()
176
+ }
177
+ } else {
178
+ // skip decoding
179
+ body = block
180
+ }
181
+
182
+ const response = okResponse(body)
183
+ response.headers.set('content-type', accept ?? 'application/json')
124
184
  return response
125
185
  }
126
186
 
127
- private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
187
+ private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
128
188
  this.log.trace('fetching %c/%s', cid, path)
129
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
130
- // return body as binary
131
- const block = await this.helia.blockstore.get(cid)
189
+
190
+ const block = await this.helia.blockstore.get(cid, options)
132
191
  let body: string | Uint8Array
133
192
 
134
- try {
135
- body = dagCborToSafeJSON(block)
136
- } catch (err) {
137
- this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
193
+ if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
194
+ // skip decoding
138
195
  body = block
196
+ } else if (accept === 'application/vnd.ipld.dag-json') {
197
+ try {
198
+ // if vnd.ipld.dag-json has been specified, convert to the format - note
199
+ // that this supports more data types than regular JSON, the content-type
200
+ // response header is set so the user knows to process it differently
201
+ const obj = ipldDagCbor.decode(block)
202
+ body = ipldDagJson.encode(obj)
203
+ } catch (err) {
204
+ this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
205
+ return notAcceptableResponse()
206
+ }
207
+ } else {
208
+ try {
209
+ body = dagCborToSafeJSON(block)
210
+ } catch (err) {
211
+ if (accept === 'application/json') {
212
+ this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
213
+
214
+ return notAcceptableResponse()
215
+ }
216
+
217
+ this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
218
+ body = block
219
+ }
139
220
  }
140
221
 
141
222
  const response = okResponse(body)
142
- response.headers.set('content-type', body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
143
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
223
+
224
+ if (accept == null) {
225
+ accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
226
+ }
227
+
228
+ response.headers.set('content-type', accept)
229
+
144
230
  return response
145
231
  }
146
232
 
147
- private async handleDagPb ({ cid, path, options, terminalElement }: FetchHandlerFunctionArg): Promise<Response> {
148
- this.log.trace('fetching %c/%s', cid, path)
233
+ private async handleDagPb ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
234
+ let terminalElement: UnixFSEntry | undefined
235
+ let ipfsRoots: CID[] | undefined
236
+
237
+ try {
238
+ const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
239
+ ipfsRoots = pathDetails.ipfsRoots
240
+ terminalElement = pathDetails.terminalElement
241
+ } catch (err) {
242
+ this.log.error('Error walking path %s', path, err)
243
+ }
244
+
149
245
  let resolvedCID = terminalElement?.cid ?? cid
150
246
  let stat: UnixFSStats
151
247
  if (terminalElement?.type === 'directory') {
@@ -154,7 +250,6 @@ export class VerifiedFetch {
154
250
  const rootFilePath = 'index.html'
155
251
  try {
156
252
  this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
157
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid, path: rootFilePath }))
158
253
  stat = await this.unixfs.stat(dirCid, {
159
254
  path: rootFilePath,
160
255
  signal: options?.signal,
@@ -172,7 +267,6 @@ export class VerifiedFetch {
172
267
  }
173
268
  }
174
269
 
175
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID, path: '' }))
176
270
  const asyncIter = this.unixfs.cat(resolvedCID, {
177
271
  signal: options?.signal,
178
272
  onProgress: options?.onProgress
@@ -185,19 +279,27 @@ export class VerifiedFetch {
185
279
  const response = okResponse(stream)
186
280
  await this.setContentType(firstChunk, path, response)
187
281
 
188
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
282
+ if (ipfsRoots != null) {
283
+ response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
284
+ }
189
285
 
190
286
  return response
191
287
  }
192
288
 
193
289
  private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
194
- this.log.trace('fetching %c/%s', cid, path)
195
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
196
- const result = await this.helia.blockstore.get(cid)
290
+ const result = await this.helia.blockstore.get(cid, options)
197
291
  const response = okResponse(result)
198
- await this.setContentType(result, path, response)
199
292
 
200
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
293
+ // if the user has specified an `Accept` header that corresponds to a raw
294
+ // type, honour that header, so for example they don't request
295
+ // `application/vnd.ipld.raw` but get `application/octet-stream`
296
+ const overriddenContentType = getOverridenRawContentType(options?.headers)
297
+ if (overriddenContentType != null) {
298
+ response.headers.set('content-type', overriddenContentType)
299
+ } else {
300
+ await this.setContentType(result, path, response)
301
+ }
302
+
201
303
  return response
202
304
  }
203
305
 
@@ -228,111 +330,112 @@ export class VerifiedFetch {
228
330
  }
229
331
 
230
332
  /**
231
- * Determines the format requested by the client, defaults to `null` if no format is requested.
232
- *
233
- * @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
234
- * @default 'raw'
235
- */
236
- private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: RequestFormatShorthand | null }): RequestFormatShorthand | null {
237
- const formatMap: Record<string, RequestFormatShorthand> = {
238
- 'vnd.ipld.raw': 'raw',
239
- 'vnd.ipld.car': 'car',
240
- 'application/x-tar': 'tar',
241
- 'application/vnd.ipld.dag-json': 'dag-json',
242
- 'application/vnd.ipld.dag-cbor': 'dag-cbor',
243
- 'application/json': 'json',
244
- 'application/cbor': 'cbor',
245
- 'vnd.ipfs.ipns-record': 'ipns-record'
246
- }
247
-
248
- if (headerFormat != null) {
249
- for (const format in formatMap) {
250
- if (headerFormat.includes(format)) {
251
- return formatMap[format]
252
- }
253
- }
254
- } else if (queryFormat != null) {
255
- return queryFormat
256
- }
257
-
258
- return null
259
- }
260
-
261
- /**
262
- * Map of format to specific handlers for that format.
263
- * These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
333
+ * If the user has not specified an Accept header or format query string arg,
334
+ * use the CID codec to choose an appropriate handler for the block data.
264
335
  */
265
- private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
266
- raw: async () => notSupportedResponse('application/vnd.ipld.raw support is not implemented'),
267
- car: this.handleIPLDCar,
268
- 'ipns-record': this.handleIPNSRecord,
269
- tar: async () => notSupportedResponse('application/x-tar support is not implemented'),
270
- 'dag-json': async () => notSupportedResponse('application/vnd.ipld.dag-json support is not implemented'),
271
- 'dag-cbor': async () => notSupportedResponse('application/vnd.ipld.dag-cbor support is not implemented'),
272
- json: async () => notSupportedResponse('application/json support is not implemented'),
273
- cbor: async () => notSupportedResponse('application/cbor support is not implemented')
274
- }
275
-
276
336
  private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
277
337
  [dagPbCode]: this.handleDagPb,
278
- [dagJsonCode]: this.handleJson,
338
+ [ipldDagJson.code]: this.handleJson,
279
339
  [jsonCode]: this.handleJson,
280
- [dagCborCode]: this.handleDagCbor,
340
+ [ipldDagCbor.code]: this.handleDagCbor,
281
341
  [rawCode]: this.handleRaw,
282
342
  [identity.code]: this.handleRaw
283
343
  }
284
344
 
285
345
  async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
346
+ this.log('fetch %s', resource)
347
+
286
348
  const options = convertOptions(opts)
287
- const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
288
- const cid = rest.cid
289
- let response: Response | undefined
290
349
 
291
- const format = this.getFormat({ headerFormat: new Headers(options?.headers).get('accept'), queryFormat: query.format ?? null })
350
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
292
351
 
293
- if (format != null) {
294
- // TODO: These should be handled last when they're returning something other than 501
295
- const formatHandler = this.formatHandlers[format]
352
+ // resolve the CID/path from the requested resource
353
+ const { path, query, cid } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
296
354
 
297
- if (formatHandler != null) {
298
- response = await formatHandler.call(this, { cid, path, options })
355
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
299
356
 
300
- if (response.status === 501) {
301
- return response
302
- }
303
- }
357
+ const requestHeaders = new Headers(options?.headers)
358
+ const incomingAcceptHeader = requestHeaders.get('accept')
359
+
360
+ if (incomingAcceptHeader != null) {
361
+ this.log('incoming accept header "%s"', incomingAcceptHeader)
304
362
  }
305
363
 
306
- let terminalElement: UnixFSEntry | undefined
307
- let ipfsRoots: CID[] | undefined
364
+ const queryFormatMapping = queryFormatToAcceptHeader(query.format)
308
365
 
309
- try {
310
- const pathDetails = await this.pathWalker(this.helia.blockstore, `${cid.toString()}/${path}`, options)
311
- ipfsRoots = pathDetails.ipfsRoots
312
- terminalElement = pathDetails.terminalElement
313
- } catch (err) {
314
- this.log.error('Error walking path %s', path, err)
315
- // return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
366
+ if (query.format != null) {
367
+ this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
368
+ }
369
+
370
+ const acceptHeader = incomingAcceptHeader ?? queryFormatMapping
371
+ const accept = selectOutputType(cid, acceptHeader)
372
+ this.log('output type %s', accept)
373
+
374
+ if (acceptHeader != null && accept == null) {
375
+ return notAcceptableResponse()
316
376
  }
317
377
 
318
- if (response == null) {
378
+ let response: Response
379
+ let reqFormat: RequestFormatShorthand | undefined
380
+
381
+ if (accept === 'application/vnd.ipfs.ipns-record') {
382
+ // the user requested a raw IPNS record
383
+ reqFormat = 'ipns-record'
384
+ response = await this.handleIPNSRecord(resource.toString(), options)
385
+ } else if (accept === 'application/vnd.ipld.car') {
386
+ // the user requested a CAR file
387
+ reqFormat = 'car'
388
+ query.download = true
389
+ query.filename = query.filename ?? `${cid.toString()}.car`
390
+ response = await this.handleCar({ cid, path, options })
391
+ } else if (accept === 'application/vnd.ipld.raw') {
392
+ // the user requested a raw block
393
+ reqFormat = 'raw'
394
+ query.download = true
395
+ query.filename = query.filename ?? `${cid.toString()}.bin`
396
+ response = await this.handleRaw({ cid, path, options })
397
+ } else if (accept === 'application/x-tar') {
398
+ // the user requested a TAR file
399
+ reqFormat = 'tar'
400
+ response = await this.handleTar({ cid, path, options })
401
+ } else {
402
+ // derive the handler from the CID type
319
403
  const codecHandler = this.codecHandlers[cid.code]
320
404
 
321
- if (codecHandler != null) {
322
- response = await codecHandler.call(this, { cid, path, options, terminalElement })
323
- } else {
405
+ if (codecHandler == null) {
324
406
  return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
325
407
  }
408
+
409
+ response = await codecHandler.call(this, { cid, path, accept, options })
326
410
  }
327
411
 
328
- response.headers.set('etag', getETag({ cid, reqFormat: format ?? undefined, weak: false }))
412
+ response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
329
413
  response.headers.set('cache-control', 'public, max-age=29030400, immutable')
330
- response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
414
+ // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
415
+ response.headers.set('X-Ipfs-Path', resource.toString())
331
416
 
332
- if (ipfsRoots != null) {
333
- response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
417
+ // set Content-Disposition header
418
+ let contentDisposition: string | undefined
419
+
420
+ // force download if requested
421
+ if (query.download === true) {
422
+ contentDisposition = 'attachment'
423
+ }
424
+
425
+ // override filename if requested
426
+ if (query.filename != null) {
427
+ if (contentDisposition == null) {
428
+ contentDisposition = 'inline'
429
+ }
430
+
431
+ contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
334
432
  }
335
- // response.headers.set('Content-Disposition', `TODO`) // https://specs.ipfs.tech/http-gateways/path-gateway/#content-disposition-response-header
433
+
434
+ if (contentDisposition != null) {
435
+ response.headers.set('Content-Disposition', contentDisposition)
436
+ }
437
+
438
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
336
439
 
337
440
  return response
338
441
  }