@helia/verified-fetch 0.0.0-7a7c0c1 → 0.0.0-7c3ce21

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