@helia/verified-fetch
Version:
A fetch-like API for obtaining verified & trustless IPFS content on the web
97 lines (82 loc) • 4.14 kB
text/typescript
import { code as rawCode } from 'multiformats/codecs/raw'
import { identity } from 'multiformats/hashes/identity'
import { getContentType } from '../utils/get-content-type.js'
import { notFoundResponse, okRangeResponse } from '../utils/responses.js'
import { PluginFatalError } from './errors.js'
import { BasePlugin } from './plugin-base.js'
import type { PluginContext } from './types.js'
/**
* These are Accept header values that will cause content type sniffing to be
* skipped and set to these values.
*/
const RAW_HEADERS = [
'application/vnd.ipld.dag-json',
'application/vnd.ipld.raw',
'application/octet-stream'
]
/**
* if the user has specified an `Accept` header, and it's in our list of
* allowable "raw" format headers, use that instead of detecting the content
* type. This avoids the user from receiving something different when they
* signal that they want to `Accept` a specific mime type.
*/
function getOverriddenRawContentType ({ headers, accept }: { headers?: HeadersInit, accept?: string }): string | undefined {
// accept has already been resolved by getResolvedAcceptHeader, if we have it, use it.
const acceptHeader = accept ?? new Headers(headers).get('accept') ?? ''
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
const acceptHeaders = acceptHeader.split(',')
.map(s => s.split(';')[0])
.map(s => s.trim())
for (const mimeType of acceptHeaders) {
if (mimeType === '*/*') {
return
}
if (RAW_HEADERS.includes(mimeType ?? '')) {
return mimeType
}
}
}
export class RawPlugin extends BasePlugin {
readonly id = 'raw-plugin'
codes: number[] = [rawCode, identity.code]
canHandle ({ cid, accept, query, byteRangeContext }: PluginContext): boolean {
this.log('checking if we can handle %c with accept %s', cid, accept)
if (byteRangeContext == null) {
return false
}
return accept === 'application/vnd.ipld.raw' || query.format === 'raw'
}
async handle (context: PluginContext & Required<Pick<PluginContext, 'byteRangeContext'>>): Promise<Response> {
const { path, resource, cid, accept, query, options } = context
const { getBlockstore, contentTypeParser } = this.pluginOptions
const session = options?.session ?? true
const log = this.log
if (accept === 'application/vnd.ipld.raw' || query.format === 'raw') {
context.reqFormat = 'raw'
context.query.download = true
context.query.filename = context.query.filename ?? `${cid.toString()}.bin`
log.trace('Set content disposition...')
} else {
log.trace('Did NOT set content disposition...')
}
if (path !== '' && cid.code === rawCode) {
log.trace('404-ing raw codec request for %c/%s', cid, path)
// throw new PluginError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths')
// return notFoundResponse(resource, 'Raw codec does not support paths')
throw new PluginFatalError('ERR_RAW_PATHS_NOT_SUPPORTED', 'Raw codec does not support paths', { response: notFoundResponse(resource, 'Raw codec does not support paths') })
}
const terminalCid = context.pathDetails?.terminalElement.cid ?? context.cid
const blockstore = getBlockstore(terminalCid, resource, session, options)
const result = await blockstore.get(terminalCid, options)
context.byteRangeContext.setBody(result)
// if the user has specified an `Accept` header that corresponds to a raw
// type, honour that header, so for example they don't request
// `application/vnd.ipld.raw` but get `application/octet-stream`
const contentType = await getContentType({ filename: query.filename, bytes: result, path, defaultContentType: getOverriddenRawContentType({ headers: options?.headers, accept }), contentTypeParser, log })
const response = okRangeResponse(resource, context.byteRangeContext.getBody(contentType), { byteRangeContext: context.byteRangeContext, log }, {
redirected: false
})
response.headers.set('content-type', context.byteRangeContext.getContentType() ?? contentType)
return response
}
}