UNPKG

@tkrotoff/fetch

Version:
347 lines (338 loc) 16 kB
'use strict'; 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;