@helia/verified-fetch
Version:
A fetch-like API for obtaining verified & trustless IPFS content on the web
326 lines • 11.1 kB
JavaScript
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