@tkrotoff/fetch
Version:
Fetch wrapper
347 lines (338 loc) • 16 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
// Should be named HTTPError or HttpError?
// - [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) have XMLHttpRequest
// - Node.js uses http: https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/
// - Deno uses Http: https://github.com/denoland/deno/blob/v1.5.3/cli/rt/01_errors.js#L116
class HttpError extends Error {
constructor(response) {
const { status, statusText } = response;
super(
// statusText can be empty: https://stackoverflow.com/q/41632077
statusText || String(status));
this.name = 'HttpError';
this.response = response;
}
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const ARRAYBUFFER_MIME_TYPE = '*/*';
const BLOB_MIME_TYPE = '*/*';
const FORMDATA_MIME_TYPE = 'multipart/form-data';
const JSON_MIME_TYPE = 'application/json';
const TEXT_MIME_TYPE = 'text/*';
function isJSONResponse(response) {
var _a;
const contentType = (_a = response.headers.get('content-type')) !== null && _a !== void 0 ? _a : '';
return contentType.includes(JSON_MIME_TYPE);
}
function extendResponsePromiseWithBodyMethods$1(responsePromise, headers) {
/* eslint-disable no-param-reassign */
function setAcceptHeader(mimeType) {
var _a;
headers.set('accept', (_a = headers.get('accept')) !== null && _a !== void 0 ? _a : mimeType);
}
responsePromise.arrayBuffer = async () => {
setAcceptHeader(ARRAYBUFFER_MIME_TYPE);
const response = await responsePromise;
return response.arrayBuffer();
};
responsePromise.blob = async () => {
setAcceptHeader(BLOB_MIME_TYPE);
const response = await responsePromise;
return response.blob();
};
responsePromise.formData = async () => {
setAcceptHeader(FORMDATA_MIME_TYPE);
const response = await responsePromise;
return response.formData();
};
responsePromise.json = async () => {
setAcceptHeader(JSON_MIME_TYPE);
const response = await responsePromise;
if (isJSONResponse(response)) {
return response.json();
}
return response.text();
};
responsePromise.text = async () => {
setAcceptHeader(TEXT_MIME_TYPE);
const response = await responsePromise;
return response.text();
};
/* eslint-enable no-param-reassign */
}
const defaults = {
init: {
// https://github.com/github/fetch/blob/v3.0.0/README.md#sending-cookies
// TODO Remove when old browsers are not supported anymore
credentials: 'same-origin'
}
};
// Can throw:
// - HttpError if !response.ok
// - TypeError if request blocked (DevTools or CORS) or network timeout (net::ERR_TIMED_OUT):
// - Firefox 68: "TypeError: "NetworkError when attempting to fetch resource.""
// - Chrome 76: "TypeError: Failed to fetch"
// - DOMException if request aborted
function request(input, headers, init, method, body) {
async function _fetch() {
// Have to wait for headers to be modified inside extendResponsePromiseWithBodyMethods
await wait(1);
const response = await fetch(input, {
...defaults.init,
...init,
headers,
method,
body
});
if (!response.ok)
throw new HttpError(response);
return response;
}
const responsePromise = _fetch();
extendResponsePromiseWithBodyMethods$1(responsePromise, headers);
return responsePromise;
}
// https://gist.github.com/userpixel/fedfe80d59aa1c096267600595ba423e
function entriesToObject(object) {
return Object.fromEntries(object.entries());
}
// FIXME Remove when support for [EdgeHTML](https://en.wikipedia.org/wiki/EdgeHTML) will be dropped
function newHeaders(init) {
// Why "?? {}"? Microsoft Edge <= 18 (EdgeHTML) throws "Invalid argument" with "new Headers(undefined)" and "new Headers(null)"
return new Headers(init !== null && init !== void 0 ? init : {});
}
function getHeaders(init) {
// We don't know if defaults.init.headers and init.headers are JSON or Headers instances
// thus we have to make the conversion
const defaultInitHeaders = entriesToObject(newHeaders(defaults.init.headers));
const initHeaders = entriesToObject(newHeaders(init === null || init === void 0 ? void 0 : init.headers));
return newHeaders({ ...defaultInitHeaders, ...initHeaders });
}
function getJSONHeaders(init) {
const headers = getHeaders(init);
headers.set('content-type', JSON_MIME_TYPE);
return headers;
}
function get(input, init) {
return request(input, getHeaders(init), init, 'GET');
}
// Should be named postJson or postJSON?
// - JS uses JSON.parse(), JSON.stringify(), toJSON()
// - Deno uses [JSON](https://github.com/denoland/deno/blob/v1.5.3/cli/dts/lib.webworker.d.ts#L387) and [Json](https://github.com/denoland/deno/blob/v1.5.3/cli/dts/lib.webworker.d.ts#L260)
//
// Record<string, unknown> is compatible with "type" not with "interface": "Index signature is missing in type 'MyInterface'"
// Best alternative is object, why? https://stackoverflow.com/a/58143592
// eslint-disable-next-line @typescript-eslint/ban-types
function postJSON(input, body, init) {
return request(input, getJSONHeaders(init), init, 'POST', JSON.stringify(body));
}
// No need to have postFormData() and friends: the browser already sets the proper request content type
// Something like "content-type: multipart/form-data; boundary=----WebKitFormBoundaryl8VQ0sfwUpJEWna3"
function post(input, body, init) {
return request(input, getHeaders(init), init, 'POST', body);
}
// eslint-disable-next-line @typescript-eslint/ban-types
function putJSON(input, body, init) {
return request(input, getJSONHeaders(init), init, 'PUT', JSON.stringify(body));
}
function put(input, body, init) {
return request(input, getHeaders(init), init, 'PUT', body);
}
// eslint-disable-next-line @typescript-eslint/ban-types
function patchJSON(input, body, init) {
return request(input, getJSONHeaders(init), init, 'PATCH', JSON.stringify(body));
}
function patch(input, body, init) {
return request(input, getHeaders(init), init, 'PATCH', body);
}
// Cannot be named delete :-/
function del(input, init) {
return request(input, getHeaders(init), init, 'DELETE');
}
function createHttpError(body, status = 0, statusText) {
return new HttpError(new Response(body, {
status,
statusText
}));
}
// Record<string, unknown> is compatible with "type" not with "interface": "Index signature is missing in type 'MyInterface'"
// Best alternative is object, why? https://stackoverflow.com/a/58143592
// eslint-disable-next-line @typescript-eslint/ban-types
function createJSONHttpError(body, status = 0, statusText) {
return new HttpError(new Response(JSON.stringify(body), {
status,
statusText,
headers: { 'content-type': JSON_MIME_TYPE }
}));
}
function extendResponsePromiseWithBodyMethods(responsePromise, response) {
// eslint-disable-next-line unicorn/no-array-for-each
['arrayBuffer', 'blob', 'formData', 'json', 'text'].forEach(methodName => {
// eslint-disable-next-line no-param-reassign
responsePromise[methodName] = () => new Promise((resolve, reject) => {
if (response.ok) {
resolve(response[methodName]());
}
else {
responsePromise.catch(() => {
// Silently catch the "root" responsePromise rejection
// We already reject just below (body method) and
// we don't want the "root" responsePromise rejection to be unhandled
});
reject(new HttpError(response));
}
});
});
}
// await get(...).text();
// ...
// const getSpy = jest.spyOn(Http, 'get').mockImplementation(() => createResponsePromise(...));
//
// await get(...);
// ...
// const getSpy = jest.spyOn(Http, 'get').mockImplementation(() => createResponsePromise(...));
//
// How to generate a HTTP error:
// const getSpy = jest.spyOn(Http, 'get').mockImplementation(() =>
// createResponsePromise('<!DOCTYPE html><title>404</title>', {
// status: 404,
// statusText: 'Not Found'
// })
// );
function createResponsePromise(body, init) {
const response = new Response(body, init);
const responsePromise = new Promise((resolve, reject) => {
if (response.ok) {
resolve(response);
}
else {
// Let's call this the "root" responsePromise rejection
// Will be silently caught if we throw inside a body method, see extendResponsePromiseWithBodyMethods
reject(new HttpError(response));
}
});
extendResponsePromiseWithBodyMethods(responsePromise, response);
return responsePromise;
}
// Record<string, unknown> is compatible with "type" not with "interface": "Index signature is missing in type 'MyInterface'"
// Best alternative is object, why? https://stackoverflow.com/a/58143592
// eslint-disable-next-line @typescript-eslint/ban-types
function createJSONResponsePromise(body, init) {
return createResponsePromise(JSON.stringify(body), init);
}
/**
* List of HTTP status codes.
*
* [List of HTTP status codes](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)
* [Rails HTTP Status Code to Symbol Mapping](https://web.archive.org/web/20131211220540/http://www.codyfauser.com/2008/7/4/rails-http-status-code-to-symbol-mapping)
*
* https://www.rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant
*
* curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
* ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
* puts "_#{m[1]}_#{m[2].delete %Q[ ]} = #{m[1]},"'
*/
exports.HttpStatus = void 0;
(function (HttpStatus) {
/*
* 1xx informational response
*/
HttpStatus[HttpStatus["_100_Continue"] = 100] = "_100_Continue";
HttpStatus[HttpStatus["_101_SwitchingProtocols"] = 101] = "_101_SwitchingProtocols";
HttpStatus[HttpStatus["_102_Processing"] = 102] = "_102_Processing";
HttpStatus[HttpStatus["_103_EarlyHints"] = 103] = "_103_EarlyHints";
/*
* 2xx success
*/
HttpStatus[HttpStatus["_200_OK"] = 200] = "_200_OK";
HttpStatus[HttpStatus["_201_Created"] = 201] = "_201_Created";
HttpStatus[HttpStatus["_202_Accepted"] = 202] = "_202_Accepted";
HttpStatus[HttpStatus["_203_NonAuthoritativeInformation"] = 203] = "_203_NonAuthoritativeInformation";
HttpStatus[HttpStatus["_204_NoContent"] = 204] = "_204_NoContent";
HttpStatus[HttpStatus["_205_ResetContent"] = 205] = "_205_ResetContent";
HttpStatus[HttpStatus["_206_PartialContent"] = 206] = "_206_PartialContent";
HttpStatus[HttpStatus["_207_MultiStatus"] = 207] = "_207_MultiStatus";
HttpStatus[HttpStatus["_208_AlreadyReported"] = 208] = "_208_AlreadyReported";
HttpStatus[HttpStatus["_226_IMUsed"] = 226] = "_226_IMUsed";
/*
* 3xx redirection
*/
HttpStatus[HttpStatus["_300_MultipleChoices"] = 300] = "_300_MultipleChoices";
HttpStatus[HttpStatus["_301_MovedPermanently"] = 301] = "_301_MovedPermanently";
HttpStatus[HttpStatus["_302_Found"] = 302] = "_302_Found";
HttpStatus[HttpStatus["_303_SeeOther"] = 303] = "_303_SeeOther";
HttpStatus[HttpStatus["_304_NotModified"] = 304] = "_304_NotModified";
HttpStatus[HttpStatus["_305_UseProxy"] = 305] = "_305_UseProxy";
HttpStatus[HttpStatus["_307_TemporaryRedirect"] = 307] = "_307_TemporaryRedirect";
HttpStatus[HttpStatus["_308_PermanentRedirect"] = 308] = "_308_PermanentRedirect";
/*
* 4xx client errors
*/
HttpStatus[HttpStatus["_400_BadRequest"] = 400] = "_400_BadRequest";
HttpStatus[HttpStatus["_401_Unauthorized"] = 401] = "_401_Unauthorized";
HttpStatus[HttpStatus["_402_PaymentRequired"] = 402] = "_402_PaymentRequired";
HttpStatus[HttpStatus["_403_Forbidden"] = 403] = "_403_Forbidden";
HttpStatus[HttpStatus["_404_NotFound"] = 404] = "_404_NotFound";
HttpStatus[HttpStatus["_405_MethodNotAllowed"] = 405] = "_405_MethodNotAllowed";
HttpStatus[HttpStatus["_406_NotAcceptable"] = 406] = "_406_NotAcceptable";
HttpStatus[HttpStatus["_407_ProxyAuthenticationRequired"] = 407] = "_407_ProxyAuthenticationRequired";
HttpStatus[HttpStatus["_408_RequestTimeout"] = 408] = "_408_RequestTimeout";
HttpStatus[HttpStatus["_409_Conflict"] = 409] = "_409_Conflict";
HttpStatus[HttpStatus["_410_Gone"] = 410] = "_410_Gone";
HttpStatus[HttpStatus["_411_LengthRequired"] = 411] = "_411_LengthRequired";
HttpStatus[HttpStatus["_412_PreconditionFailed"] = 412] = "_412_PreconditionFailed";
HttpStatus[HttpStatus["_413_PayloadTooLarge"] = 413] = "_413_PayloadTooLarge";
HttpStatus[HttpStatus["_414_URITooLong"] = 414] = "_414_URITooLong";
HttpStatus[HttpStatus["_415_UnsupportedMediaType"] = 415] = "_415_UnsupportedMediaType";
HttpStatus[HttpStatus["_416_RangeNotSatisfiable"] = 416] = "_416_RangeNotSatisfiable";
HttpStatus[HttpStatus["_417_ExpectationFailed"] = 417] = "_417_ExpectationFailed";
HttpStatus[HttpStatus["_421_MisdirectedRequest"] = 421] = "_421_MisdirectedRequest";
HttpStatus[HttpStatus["_422_UnprocessableEntity"] = 422] = "_422_UnprocessableEntity";
HttpStatus[HttpStatus["_423_Locked"] = 423] = "_423_Locked";
HttpStatus[HttpStatus["_424_FailedDependency"] = 424] = "_424_FailedDependency";
HttpStatus[HttpStatus["_425_TooEarly"] = 425] = "_425_TooEarly";
HttpStatus[HttpStatus["_426_UpgradeRequired"] = 426] = "_426_UpgradeRequired";
HttpStatus[HttpStatus["_428_PreconditionRequired"] = 428] = "_428_PreconditionRequired";
HttpStatus[HttpStatus["_429_TooManyRequests"] = 429] = "_429_TooManyRequests";
HttpStatus[HttpStatus["_431_RequestHeaderFieldsTooLarge"] = 431] = "_431_RequestHeaderFieldsTooLarge";
HttpStatus[HttpStatus["_451_UnavailableForLegalReasons"] = 451] = "_451_UnavailableForLegalReasons";
/*
* 5xx server errors
*/
HttpStatus[HttpStatus["_500_InternalServerError"] = 500] = "_500_InternalServerError";
HttpStatus[HttpStatus["_501_NotImplemented"] = 501] = "_501_NotImplemented";
HttpStatus[HttpStatus["_502_BadGateway"] = 502] = "_502_BadGateway";
HttpStatus[HttpStatus["_503_ServiceUnavailable"] = 503] = "_503_ServiceUnavailable";
HttpStatus[HttpStatus["_504_GatewayTimeout"] = 504] = "_504_GatewayTimeout";
HttpStatus[HttpStatus["_505_HTTPVersionNotSupported"] = 505] = "_505_HTTPVersionNotSupported";
HttpStatus[HttpStatus["_506_VariantAlsoNegotiates"] = 506] = "_506_VariantAlsoNegotiates";
HttpStatus[HttpStatus["_507_InsufficientStorage"] = 507] = "_507_InsufficientStorage";
HttpStatus[HttpStatus["_508_LoopDetected"] = 508] = "_508_LoopDetected";
HttpStatus[HttpStatus["_510_NotExtended"] = 510] = "_510_NotExtended";
HttpStatus[HttpStatus["_511_NetworkAuthenticationRequired"] = 511] = "_511_NetworkAuthenticationRequired";
/*
* Unofficial codes
*/
// A deprecated response used by the Spring Framework when a method has failed
HttpStatus[HttpStatus["_420_MethodFailure"] = 420] = "_420_MethodFailure";
})(exports.HttpStatus || (exports.HttpStatus = {}));
exports.HttpError = HttpError;
exports.JSON_MIME_TYPE = JSON_MIME_TYPE;
exports.createHttpError = createHttpError;
exports.createJSONHttpError = createJSONHttpError;
exports.createJSONResponsePromise = createJSONResponsePromise;
exports.createResponsePromise = createResponsePromise;
exports.defaults = defaults;
exports.del = del;
exports.entriesToObject = entriesToObject;
exports.get = get;
exports.isJSONResponse = isJSONResponse;
exports.patch = patch;
exports.patchJSON = patchJSON;
exports.post = post;
exports.postJSON = postJSON;
exports.put = put;
exports.putJSON = putJSON;