-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support
Accept
header in @helia/verified-fetch
(#438)
Let users get raw data back from CIDs that would otherwise trigger decoding as JSON or CBOR etc by specifying an `Accept` header. ```typescript const res = await verifiedFetch(cid, { headers: { accept: 'application/octet-stream' } }) console.info(res.headers.get('accept')) // application/octet-stream ``` Make sure the content-type matches the accept header: ```typescript const res = await verifiedFetch(cid, { headers: { accept: 'application/vnd.ipld.raw' } }) console.info(res.headers.get('accept')) // application/vnd.ipld.raw ``` Support multiple values, match the first one: ```typescript const res = await verifiedFetch(cid, { headers: { accept: 'application/vnd.ipld.raw, application/octet-stream, */*' } }) console.info(res.headers.get('accept')) // application/vnd.ipld.raw ``` If they specify an Accept header we can't honor, return a [406](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406): ```typescript const res = await verifiedFetch(cid, { headers: { accept: 'application/what-even-is-this' } }) console.info(res.status) // 406 ``` --------- Co-authored-by: Russell Dempsey <[email protected]>
- Loading branch information
1 parent
f9b1ffe
commit 54c4383
Showing
15 changed files
with
1,000 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** | ||
* Takes a filename URL param and returns a string for use in a | ||
* `Content-Disposition` header | ||
*/ | ||
export function getContentDispositionFilename (filename: string): string { | ||
const asciiOnly = replaceNonAsciiCharacters(filename) | ||
|
||
if (asciiOnly === filename) { | ||
return `filename="${filename}"` | ||
} | ||
|
||
return `filename="${asciiOnly}"; filename*=UTF-8''${encodeURIComponent(filename)}` | ||
} | ||
|
||
function replaceNonAsciiCharacters (filename: string): string { | ||
// eslint-disable-next-line no-control-regex | ||
return filename.replace(/[^\x00-\x7F]/g, '_') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export function okResponse (body?: BodyInit | null): Response { | ||
return new Response(body, { | ||
status: 200, | ||
statusText: 'OK' | ||
}) | ||
} | ||
|
||
export function notSupportedResponse (body?: BodyInit | null): Response { | ||
const response = new Response(body, { | ||
status: 501, | ||
statusText: 'Not Implemented' | ||
}) | ||
response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header | ||
return response | ||
} | ||
|
||
export function notAcceptableResponse (body?: BodyInit | null): Response { | ||
return new Response(body, { | ||
status: 406, | ||
statusText: '406 Not Acceptable' | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { code as dagCborCode } from '@ipld/dag-cbor' | ||
import { code as dagJsonCode } from '@ipld/dag-json' | ||
import { code as dagPbCode } from '@ipld/dag-pb' | ||
import { code as jsonCode } from 'multiformats/codecs/json' | ||
import { code as rawCode } from 'multiformats/codecs/raw' | ||
import type { RequestFormatShorthand } from '../types.js' | ||
import type { CID } from 'multiformats/cid' | ||
|
||
/** | ||
* This maps supported response types for each codec supported by verified-fetch | ||
*/ | ||
const CID_TYPE_MAP: Record<number, string[]> = { | ||
[dagCborCode]: [ | ||
'application/json', | ||
'application/vnd.ipld.dag-cbor', | ||
'application/cbor', | ||
'application/vnd.ipld.dag-json', | ||
'application/octet-stream', | ||
'application/vnd.ipld.raw', | ||
'application/vnd.ipfs.ipns-record', | ||
'application/vnd.ipld.car' | ||
], | ||
[dagJsonCode]: [ | ||
'application/json', | ||
'application/vnd.ipld.dag-cbor', | ||
'application/cbor', | ||
'application/vnd.ipld.dag-json', | ||
'application/octet-stream', | ||
'application/vnd.ipld.raw', | ||
'application/vnd.ipfs.ipns-record', | ||
'application/vnd.ipld.car' | ||
], | ||
[jsonCode]: [ | ||
'application/json', | ||
'application/vnd.ipld.dag-cbor', | ||
'application/cbor', | ||
'application/vnd.ipld.dag-json', | ||
'application/octet-stream', | ||
'application/vnd.ipld.raw', | ||
'application/vnd.ipfs.ipns-record', | ||
'application/vnd.ipld.car' | ||
], | ||
[dagPbCode]: [ | ||
'application/octet-stream', | ||
'application/json', | ||
'application/vnd.ipld.dag-cbor', | ||
'application/cbor', | ||
'application/vnd.ipld.dag-json', | ||
'application/vnd.ipld.raw', | ||
'application/vnd.ipfs.ipns-record', | ||
'application/vnd.ipld.car', | ||
'application/x-tar' | ||
], | ||
[rawCode]: [ | ||
'application/octet-stream', | ||
'application/vnd.ipld.raw', | ||
'application/vnd.ipfs.ipns-record', | ||
'application/vnd.ipld.car' | ||
] | ||
} | ||
|
||
/** | ||
* Selects an output mime-type based on the CID and a passed `Accept` header | ||
*/ | ||
export function selectOutputType (cid: CID, accept?: string): string | undefined { | ||
const cidMimeTypes = CID_TYPE_MAP[cid.code] | ||
|
||
if (accept != null) { | ||
return chooseMimeType(accept, cidMimeTypes) | ||
} | ||
} | ||
|
||
function chooseMimeType (accept: string, validMimeTypes: string[]): string | undefined { | ||
const requestedMimeTypes = accept | ||
.split(',') | ||
.map(s => { | ||
const parts = s.trim().split(';') | ||
|
||
return { | ||
mimeType: `${parts[0]}`.trim(), | ||
weight: parseQFactor(parts[1]) | ||
} | ||
}) | ||
.sort((a, b) => { | ||
if (a.weight === b.weight) { | ||
return 0 | ||
} | ||
|
||
if (a.weight > b.weight) { | ||
return -1 | ||
} | ||
|
||
return 1 | ||
}) | ||
.map(s => s.mimeType) | ||
|
||
for (const headerFormat of requestedMimeTypes) { | ||
for (const mimeType of validMimeTypes) { | ||
if (headerFormat.includes(mimeType)) { | ||
return mimeType | ||
} | ||
|
||
if (headerFormat === '*/*') { | ||
return mimeType | ||
} | ||
|
||
if (headerFormat.startsWith('*/') && mimeType.split('/')[1] === headerFormat.split('/')[1]) { | ||
return mimeType | ||
} | ||
|
||
if (headerFormat.endsWith('/*') && mimeType.split('/')[0] === headerFormat.split('/')[0]) { | ||
return mimeType | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Parses q-factor weighting from the accept header to allow letting some mime | ||
* types take precedence over others. | ||
* | ||
* If the q-factor for an acceptable mime representation is omitted it defaults | ||
* to `1`. | ||
* | ||
* All specified values should be in the range 0-1. | ||
* | ||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#q | ||
*/ | ||
function parseQFactor (str?: string): number { | ||
if (str != null) { | ||
str = str.trim() | ||
} | ||
|
||
if (str == null || !str.startsWith('q=')) { | ||
return 1 | ||
} | ||
|
||
const factor = parseFloat(str.replace('q=', '')) | ||
|
||
if (isNaN(factor)) { | ||
return 0 | ||
} | ||
|
||
return factor | ||
} | ||
|
||
const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = { | ||
raw: 'application/vnd.ipld.raw', | ||
car: 'application/vnd.ipld.car', | ||
'dag-json': 'application/vnd.ipld.dag-json', | ||
'dag-cbor': 'application/vnd.ipld.dag-cbor', | ||
json: 'application/json', | ||
cbor: 'application/cbor', | ||
'ipns-record': 'application/vnd.ipfs.ipns-record', | ||
tar: 'application/x-tar' | ||
} | ||
|
||
/** | ||
* Converts a `format=...` query param to a mime type as would be found in the | ||
* `Accept` header, if a valid mapping is available | ||
*/ | ||
export function queryFormatToAcceptHeader (format?: RequestFormatShorthand): string | undefined { | ||
if (format != null) { | ||
return FORMAT_TO_MIME_TYPE[format] | ||
} | ||
} |
Oops, something went wrong.