UNPKG

@helia/verified-fetch

Version:

A fetch-like API for obtaining verified & trustless IPFS content on the web

326 lines 11.1 kB
import itToBrowserReadableStream from 'it-to-browser-readablestream'; import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'; import { rangeToOffsetAndLength } from "./get-offset-and-length.js"; import { getContentRangeHeader } from "./response-headers.js"; function setField(response, name, value) { Object.defineProperty(response, name, { enumerable: true, configurable: false, set: () => { }, get: () => value }); } function setType(response, value) { if (response.type !== value) { setField(response, 'type', value); } } function setUrl(response, value) { value = value.toString(); const fragmentStart = value.indexOf('#'); if (fragmentStart > -1) { value = value.substring(0, fragmentStart); } if (response.url !== value) { setField(response, 'url', value); } } function setRedirected(response) { setField(response, 'redirected', true); } export function okResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), status: 200, statusText: 'OK' }); if (init?.redirected === true) { setRedirected(response); } setType(response, 'basic'); setUrl(response, url); return response; } export function internalServerErrorResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), status: 500, statusText: 'Internal Server Error' }); response.headers.set('X-Content-Type-Options', 'nosniff'); // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header setType(response, 'basic'); setUrl(response, url); return response; } /** * A 504 Gateway Timeout for when a request made to an upstream server timed out */ export function gatewayTimeoutResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), status: 504, statusText: 'Gateway Timeout' }); setType(response, 'basic'); setUrl(response, url); return response; } /** * A 502 Bad Gateway is for when an invalid response was received from an * upstream server. */ export function badGatewayResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), status: 502, statusText: 'Bad Gateway' }); setType(response, 'basic'); setUrl(response, url); return response; } export function notImplementedResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), 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 setType(response, 'basic'); setUrl(response, url); return response; } export function notAcceptableResponse(url, requested, acceptable, init) { const headers = new Headers(init?.headers); headers.set('content-type', 'application/json'); const response = new Response(JSON.stringify({ requested: requested.map(contentType => contentType.mediaType), acceptable: acceptable.map(contentType => contentType.mediaType) }), { ...(init ?? {}), status: 406, statusText: 'Not Acceptable', headers }); setType(response, 'basic'); setUrl(response, url); return response; } export function notFoundResponse(url, body, init) { const response = new Response(body, { ...(init ?? {}), status: 404, statusText: 'Not Found' }); setType(response, 'basic'); setUrl(response, url); return response; } function isArrayOfErrors(body) { return Array.isArray(body) && body.every(e => e instanceof Error); } export function badRequestResponse(url, errors, init) { // stacktrace of the single error, or the stacktrace of the last error in the array let stack; let convertedErrors; if (isArrayOfErrors(errors)) { stack = errors[errors.length - 1].stack; convertedErrors = errors.map(e => ({ message: e.message, stack: e.stack ?? '' })); } else if (errors instanceof Error) { stack = errors.stack; convertedErrors = [{ message: errors.message, stack: errors.stack ?? '' }]; } const bodyJson = JSON.stringify({ stack, errors: convertedErrors }); const response = new Response(bodyJson, { status: 400, statusText: 'Bad Request', ...(init ?? {}), headers: { ...(init?.headers ?? {}), 'Content-Type': 'application/json' } }); setType(response, 'basic'); setUrl(response, url); return response; } export function movedPermanentlyResponse(url, location, init) { const response = new Response(null, { ...(init ?? {}), status: 301, statusText: 'Moved Permanently', headers: { ...(init?.headers ?? {}), location } }); setType(response, 'basic'); setUrl(response, url); return response; } export function notModifiedResponse(url, headers, init) { const response = new Response(null, { ...(init ?? {}), status: 304, statusText: 'Not Modified' }); // These fields would be present on a 200 and must be sent as part of a 304 // for the same resource // https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified const copyHeaders = [ 'cache-control', 'content-location', 'date', 'etag', 'expires', 'vary' ]; copyHeaders.forEach(key => { const value = headers.get(key); if (value != null) { response.headers.set(key, value); } }); setType(response, 'basic'); setUrl(response, url); return response; } export function partialContentResponse(url, getSlice, range, documentSize, init) { let response; if (range.ranges.length === 1) { response = singleRangeResponse(url, getSlice, range.ranges[0], documentSize, init); } else if (range.ranges.length > 1) { response = multiRangeResponse(url, getSlice, range, documentSize, init); } else { return notSatisfiableResponse(url, documentSize); } if (init?.redirected === true) { setRedirected(response); } setType(response, 'basic'); setUrl(response, url); return response; } function singleRangeResponse(url, getSlice, range, documentSize, init) { try { // create headers object with any initial headers from init const headers = new Headers(init?.headers); const { offset, length } = rangeToOffsetAndLength(documentSize, range.start, range.end); headers.set('content-length', `${length}`); // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range headers.set('content-range', getContentRangeHeader(documentSize, range.start, range.end)); const stream = itToBrowserReadableStream(getSlice(offset, length)); return new Response(stream, { ...(init ?? {}), status: 206, statusText: 'Partial Content', headers }); } catch (err) { if (err.name === 'InvalidRangeError') { return notSatisfiableResponse(url, documentSize, init); } return internalServerErrorResponse(url, '', init); } } /** * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests */ function multiRangeResponse(url, getSlice, range, documentSize, init) { // create headers object with any initial headers from init const headers = new Headers(init?.headers); const contentType = headers.get('content-type'); if (contentType == null) { throw new Error('Content-Type header must be set'); } headers.delete('content-type'); let contentLength = 0n; // calculate content range based on range headers const rangeHeaders = range.ranges.map(({ start, end }) => { const header = uint8ArrayFromString([ `--${range.multipartBoundary}`, // content-type of multipart part `Content-Type: ${contentType}`, // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range `Content-Range: ${getContentRangeHeader(documentSize, start, end)}`, '', '' ].join('\r\n')); contentLength += BigInt(header.byteLength) + (BigInt(end ?? documentSize) - BigInt(start ?? 0)); return header; }); const trailer = uint8ArrayFromString([ `--${range.multipartBoundary}--`, '' ].join('\r\n')); contentLength += BigInt(trailer.byteLength); // content length is the expected length of all multipart parts headers.set('content-length', `${contentLength}`); // content type of response is multipart headers.set('content-type', `multipart/byteranges; boundary=${range.multipartBoundary}`); const stream = itToBrowserReadableStream(async function* () { for (let i = 0; i < rangeHeaders.length; i++) { yield rangeHeaders[i]; const { offset, length } = rangeToOffsetAndLength(documentSize, range.ranges[i].start, range.ranges[i].end); yield* getSlice(offset, length); yield uint8ArrayFromString('\r\n'); } yield trailer; }()); return new Response(stream, { ...(init ?? {}), status: 206, statusText: 'Partial Content', headers }); } /** * We likely need to catch errors handled by upstream helia libraries if * range-request throws an error. Some examples: * * - The range is out of bounds * - The range is invalid * - The range is not supported for the given type */ export function notSatisfiableResponse(url, documentSize, init) { const headers = new Headers(init?.headers); if (documentSize != null) { headers.set('content-range', `bytes */${documentSize}`); } const response = new Response('Range Not Satisfiable', { ...init, headers, status: 416, statusText: 'Range Not Satisfiable' }); setType(response, 'basic'); setUrl(response, url); return response; } /** * Error to indicate that request was formally correct, but Gateway is unable to * return requested data under the additional (usually cache-related) conditions * sent by the client. * * @see https://specs.ipfs.tech/http-gateways/path-gateway/#412-precondition-failed */ export function preconditionFailedResponse(url, init) { const headers = new Headers(init?.headers); const response = new Response('Precondition Failed', { ...init, headers, status: 412, statusText: 'Precondition Failed' }); setType(response, 'basic'); setUrl(response, url); return response; } //# sourceMappingURL=responses.js.map