Skip to content

Commit 97724a7

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/handle-accept-header-for-raw-types
2 parents 67ddf99 + 8db7792 commit 97724a7

File tree

6 files changed

+83
-8
lines changed

6 files changed

+83
-8
lines changed

packages/verified-fetch/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type RequestFormatShorthand = 'raw' | 'car' | 'tar' | 'ipns-record' | 'dag-json' | 'dag-cbor' | 'json' | 'cbor'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { RequestFormatShorthand } from '../types.js'
2+
import type { CID } from 'multiformats/cid'
3+
4+
interface GetETagArg {
5+
cid: CID
6+
reqFormat?: RequestFormatShorthand
7+
rangeStart?: number
8+
rangeEnd?: number
9+
/**
10+
* Weak Etag is used when we can't guarantee byte-for-byte-determinism (generated, or mutable content).
11+
* Some examples:
12+
* - IPNS requests
13+
* - CAR streamed with blocks in non-deterministic order
14+
* - TAR streamed with files in non-deterministic order
15+
*/
16+
weak?: boolean
17+
}
18+
19+
/**
20+
* etag
21+
* you need to wrap cid with ""
22+
* we use strong Etags for immutable responses and weak one (prefixed with W/ ) for mutable/generated ones (ipns and generated HTML).
23+
* block and car responses should have different etag than deserialized one, so you can add some prefix like we do in existing gateway
24+
*
25+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
26+
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
27+
*/
28+
export function getETag ({ cid, reqFormat, weak, rangeStart, rangeEnd }: GetETagArg): string {
29+
const prefix = weak === true ? 'W/' : ''
30+
let suffix = reqFormat == null ? '' : `.${reqFormat}`
31+
if (rangeStart != null || rangeEnd != null) {
32+
suffix += `.${rangeStart ?? '0'}-${rangeEnd ?? 'N'}`
33+
}
34+
35+
return `${prefix}"${cid.toString()}${suffix}"`
36+
}

packages/verified-fetch/src/utils/get-format.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { code as dagCborCode } from '@ipld/dag-cbor'
22
import { code as dagJsonCode } from '@ipld/dag-json'
3+
import type { RequestFormatShorthand } from '../types.js'
34
import type { CID } from 'multiformats/cid'
45

5-
export type FORMAT = 'raw' | 'car' | 'dag-json' | 'dag-cbor' | 'json' | 'cbor' | 'ipns-record' | 'tar'
6-
76
const FORMATS: string[] = [
87
'raw', 'car', 'dag-json', 'dag-cbor', 'json', 'cbor', 'ipns-record', 'tar'
98
]
109

11-
function isSupportedFormat (format: string): format is FORMAT {
10+
function isSupportedFormat (format: string): format is RequestFormatShorthand {
1211
return FORMATS.includes(format)
1312
}
1413

15-
const FORMAT_MAP: Record<string, FORMAT> = {
14+
const FORMAT_MAP: Record<string, RequestFormatShorthand> = {
1615
// https://www.iana.org/assignments/media-types/application/vnd.ipld.raw
1716
'application/vnd.ipld.raw': 'raw',
1817
'application/octet-stream': 'raw',
@@ -33,7 +32,7 @@ const FORMAT_MAP: Record<string, FORMAT> = {
3332
'application/x-tar': 'tar'
3433
}
3534

36-
const MIME_TYPE_MAP: Record<FORMAT, string> = {
35+
const MIME_TYPE_MAP: Record<RequestFormatShorthand, string> = {
3736
raw: 'application/octet-stream',
3837
car: 'application/vnd.ipld.car',
3938
'dag-json': 'application/vnd.ipld.dag-json',
@@ -45,7 +44,7 @@ const MIME_TYPE_MAP: Record<FORMAT, string> = {
4544
}
4645

4746
interface UserFormat {
48-
format: FORMAT
47+
format: RequestFormatShorthand
4948
mimeType: string
5049
}
5150

packages/verified-fetch/src/utils/parse-url-string.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { peerIdFromString } from '@libp2p/peer-id'
22
import { CID } from 'multiformats/cid'
33
import { TLRU } from './tlru.js'
4+
import type { RequestFormatShorthand } from '../types.js'
45
import type { IPNS, IPNSRoutingEvents, ResolveDnsLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
56
import type { ComponentLogger } from '@libp2p/interface'
67
import type { ProgressOptions } from 'progress-events'
@@ -16,11 +17,15 @@ export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEv
1617

1718
}
1819

20+
export interface ParsedUrlQuery extends Record<string, string | unknown> {
21+
format?: RequestFormatShorthand
22+
}
23+
1924
export interface ParsedUrlStringResults {
2025
protocol: string
2126
path: string
2227
cid: CID
23-
query: Record<string, string>
28+
query: ParsedUrlQuery
2429
}
2530

2631
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/$?]+)\/?(?<path>[^$?]*)\??(?<queryString>.*)$/

packages/verified-fetch/src/verified-fetch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { code as rawCode } from 'multiformats/codecs/raw'
99
import { identity } from 'multiformats/hashes/identity'
1010
import { CustomProgressEvent } from 'progress-events'
1111
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
12+
import { getETag } from './utils/get-e-tag.js'
1213
import { getFormat } from './utils/get-format.js'
1314
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
1415
import { parseResource } from './utils/parse-resource.js'
@@ -383,7 +384,7 @@ export class VerifiedFetch {
383384
return notAcceptableResponse()
384385
}
385386

386-
response.headers.set('etag', cid.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#etag-response-header
387+
response.headers.set('etag', getETag({ cid, reqFormat: format?.format, weak: false }))
387388
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
388389
response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
389390

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect } from 'aegir/chai'
2+
import { CID } from 'multiformats/cid'
3+
import { getETag } from '../src/utils/get-e-tag.js'
4+
5+
const cidString = 'QmQJ8fxavY54CUsxMSx9aE9Rdcmvhx8awJK2jzJp4iAqCr'
6+
const testCID = CID.parse(cidString)
7+
8+
describe('getETag', () => {
9+
it('CID eTag', () => {
10+
expect(getETag({ cid: testCID, weak: true })).to.equal(`W/"${cidString}"`)
11+
expect(getETag({ cid: testCID, weak: false })).to.equal(`"${cidString}"`)
12+
})
13+
14+
it('should return ETag with CID and format suffix', () => {
15+
expect(getETag({ cid: testCID, reqFormat: 'raw' })).to.equal(`"${cidString}.raw"`)
16+
expect(getETag({ cid: testCID, reqFormat: 'json' })).to.equal(`"${cidString}.json"`)
17+
})
18+
19+
it('should return ETag with CID and range suffix', () => {
20+
expect(getETag({ cid: testCID, weak: true, reqFormat: 'car', rangeStart: 10, rangeEnd: 20 })).to.equal(`W/"${cidString}.car.10-20"`)
21+
expect(getETag({ cid: testCID, weak: false, reqFormat: 'car', rangeStart: 10, rangeEnd: 20 })).to.equal(`"${cidString}.car.10-20"`)
22+
})
23+
24+
it('should return ETag with CID, format and range suffix', () => {
25+
expect(getETag({ cid: testCID, reqFormat: 'raw', weak: false, rangeStart: 10, rangeEnd: 20 })).to.equal(`"${cidString}.raw.10-20"`)
26+
})
27+
28+
it('should handle undefined rangeStart and rangeEnd', () => {
29+
expect(getETag({ cid: testCID, reqFormat: 'raw', weak: false, rangeStart: undefined, rangeEnd: undefined })).to.equal(`"${cidString}.raw"`)
30+
expect(getETag({ cid: testCID, reqFormat: 'raw', weak: false, rangeStart: 55, rangeEnd: undefined })).to.equal(`"${cidString}.raw.55-N"`)
31+
expect(getETag({ cid: testCID, reqFormat: 'raw', weak: false, rangeStart: undefined, rangeEnd: 77 })).to.equal(`"${cidString}.raw.0-77"`)
32+
})
33+
})

0 commit comments

Comments
 (0)