UNPKG

@applitools/req

Version:

Applitools fetch-based request library

295 lines (294 loc) 15.8 kB
import { AbortController } from 'abort-controller'; import { stop } from './stop.js'; import { makeAgent } from './agent.js'; import { AbortCode, RequestTimeoutError, ConnectionTimeoutError } from './req-errors.js'; import globalFetch, { Request, Headers, Response } from './fetch.js'; import * as utils from '@applitools/utils'; import { Buffer } from 'buffer'; const disableHttpAgentReuse = process.env.APPLITOOLS_DISABLE_AGENT_CACHIFY; /** * Helper function that will create {@link req} function with predefined options * @example const req = makeReq({baseUrl: 'http://localhost:2107'}) */ export function makeReq(baseOptions) { return (location, options) => req(location, mergeOptions(baseOptions, options !== null && options !== void 0 ? options : {})); } export async function req(input, ...requestOptions) { const options = mergeOptions({}, ...requestOptions); let abortCode; if (options.baseUrl && !options.baseUrl.endsWith('/')) options.baseUrl += '/'; if (options.headers) options.headers = Object.fromEntries(Object.entries(options.headers).filter(([_, value]) => value)); else if (!disableHttpAgentReuse) options.headers = {}; if (!disableHttpAgentReuse) if (!Object.keys(options.headers).some(header => header.toLowerCase() === 'connection')) options.headers = { Connection: 'keep-alive', ...options.headers, }; if (options.retry) options.retry = utils.types.isArray(options.retry) ? options.retry : [options.retry]; if (options.hooks) options.hooks = utils.types.isArray(options.hooks) ? options.hooks : [options.hooks]; const connectionController = new AbortController(); const connectionTimer = options.connectionTimeout ? setTimeout(() => { abortCode = AbortCode.connectionTimeout; connectionController.abort(); }, options.connectionTimeout) : null; try { return await singleReq(input, options); } finally { if (connectionTimer) clearTimeout(connectionTimer); } async function singleReq(input, options) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; const url = new URL(String((_a = input.url) !== null && _a !== void 0 ? _a : input), options.baseUrl); const fetch = (_b = options.fetch) !== null && _b !== void 0 ? _b : globalFetch; let optionsFallbacks = []; if (options.fallbacks) optionsFallbacks = utils.types.isArray(options.fallbacks) ? options.fallbacks : [options.fallbacks]; const fb = optionsFallbacks.find(fallback => { var _a; return (_a = fallback.cache) === null || _a === void 0 ? void 0 : _a.get(url.origin); }); if (fb === null || fb === void 0 ? void 0 : fb.updateOptions) options = await fb.updateOptions({ options }); const requestController = new AbortController(); const timeout = calculateTimeout(options.requestTimeout, options.body, options); const requestTimer = timeout ? setTimeout(() => { abortCode !== null && abortCode !== void 0 ? abortCode : (abortCode = AbortCode.requestTimeout); requestController.abort(); }, timeout) : null; if (connectionController.signal.aborted) requestController.abort(); connectionController.signal.onabort = () => requestController.abort(); if (options.signal) { if (options.signal.aborted) requestController.abort(); options.signal.onabort = () => requestController.abort(); } if (options.query) { Object.entries(options.query).forEach(([key, value]) => { if (!utils.types.isNull(value)) url.searchParams.set(key, String(value)); }); } const extraHeaders = {}; if (utils.types.isPlainObject(options.body) || utils.types.isArray(options.body) || options.body === null) { options.body = JSON.stringify(options.body); extraHeaders['content-type'] = 'application/json'; } let request = new Request(url, { method: (_c = options.method) !== null && _c !== void 0 ? _c : input.method, headers: { ...extraHeaders, ...Object.fromEntries((_e = (_d = input.headers) === null || _d === void 0 ? void 0 : _d.entries()) !== null && _e !== void 0 ? _e : []), ...Object.fromEntries(new Headers(options.headers).entries()), }, body: (_f = options.body) !== null && _f !== void 0 ? _f : input.body, highWaterMark: 1024 * 1024 * 100 + 1, agent: makeAgent({ proxy: options.proxy, useDnsCache: options.useDnsCache, keepAliveOptions: options.keepAliveOptions, }), signal: requestController.signal, }); request = await beforeRequest({ request, options }); try { let response = await fetch(request); // if the request has a fallback try it if (!response.ok && optionsFallbacks.length > 0) { const fallbackStrategy = optionsFallbacks[0]; const shouldFallback = await fallbackStrategy.shouldFallbackCondition({ request, response }); const fallbackOptions = shouldFallback && (await ((_g = fallbackStrategy === null || fallbackStrategy === void 0 ? void 0 : fallbackStrategy.updateOptions) === null || _g === void 0 ? void 0 : _g.call(fallbackStrategy, { options: { ...options, fallbacks: optionsFallbacks.slice(1) }, }))); if (fallbackOptions) { const fallbackStrategyResponse = await singleReq(request, fallbackOptions); (_h = fallbackStrategy.cache) !== null && _h !== void 0 ? _h : (fallbackStrategy.cache = new Map()); fallbackStrategy.cache.set(new URL(request.url).origin, fallbackStrategyResponse.ok); return fallbackStrategyResponse; } } // if the request has to be retried due to status code const retry = await ((_j = options.retry) === null || _j === void 0 ? void 0 : _j.reduce(async (prev, retry) => { var _a, _b; const result = await prev; return (result !== null && result !== void 0 ? result : ((((_a = retry.statuses) === null || _a === void 0 ? void 0 : _a.includes(response.status)) || (await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { response })))) && (!retry.limit || !retry.attempt || retry.attempt < retry.limit) ? retry : null)); }, Promise.resolve(null))); if (retry) { (_k = retry.attempt) !== null && _k !== void 0 ? _k : (retry.attempt = 0); const delay = response.headers.has('Retry-After') ? Number(response.headers.get('Retry-After')) * 1000 : utils.types.isArray(retry.timeout) ? retry.timeout[Math.min(retry.attempt, retry.timeout.length - 1)] : (_l = retry.timeout) !== null && _l !== void 0 ? _l : 0; await utils.general.sleep(delay); retry.attempt += 1; const retryRequest = await beforeRetry({ request, response, attempt: retry.attempt, stop, options }); if (retryRequest !== stop) { return singleReq(retryRequest, options); } } response = await afterResponse({ request, response, options }); return response; } catch (error) { if (abortCode === AbortCode.requestTimeout) error = new RequestTimeoutError(); else if (abortCode === AbortCode.connectionTimeout) error = new ConnectionTimeoutError(); // if the request has to be retried due to network error const retry = await ((_m = options.retry) === null || _m === void 0 ? void 0 : _m.reduce((prev, retry) => { return prev.then(async (result) => { var _a, _b; return (result !== null && result !== void 0 ? result : ((((_a = retry.codes) === null || _a === void 0 ? void 0 : _a.includes(error.code)) || (await ((_b = retry.validate) === null || _b === void 0 ? void 0 : _b.call(retry, { error })))) && (!retry.limit || !retry.attempt || retry.attempt < retry.limit))) ? retry : null; }); }, Promise.resolve(null))); if (retry) { (_o = retry.attempt) !== null && _o !== void 0 ? _o : (retry.attempt = 0); const delay = utils.types.isArray(retry.timeout) ? retry.timeout[Math.min(retry.attempt, retry.timeout.length)] : (_p = retry.timeout) !== null && _p !== void 0 ? _p : 0; await utils.general.sleep(delay); retry.attempt = retry.attempt + 1; const retryRequest = await beforeRetry({ request, error, attempt: retry.attempt, stop, options }); if (retryRequest !== stop) { return singleReq(retryRequest, options); } } error = await afterError({ request, error, options }); throw error; } finally { if (options.signal) options.signal.onabort = null; if (requestTimer) clearTimeout(requestTimer); } } } function mergeOptions(baseOptions, ...options) { var _a; const mergedOptions = options.reduce((baseOptions, options) => ({ ...baseOptions, ...options, query: { ...baseOptions.query, ...options === null || options === void 0 ? void 0 : options.query }, headers: { ...baseOptions.headers, ...options === null || options === void 0 ? void 0 : options.headers }, retry: [ ...(baseOptions.retry ? [].concat(baseOptions.retry) : []), ...((options === null || options === void 0 ? void 0 : options.retry) ? [].concat(options.retry) : []), ], hooks: [ ...(baseOptions.hooks ? [].concat(baseOptions.hooks) : []), ...((options === null || options === void 0 ? void 0 : options.hooks) ? [].concat(options.hooks) : []), ], fallbacks: [ ...(baseOptions.fallbacks ? [].concat(baseOptions.fallbacks) : []), ...((options === null || options === void 0 ? void 0 : options.fallbacks) ? [].concat(options.fallbacks) : []), ], }), baseOptions); return ((_a = mergedOptions.hooks) !== null && _a !== void 0 ? _a : []).reduce((options, hooks) => { var _a, _b; return (_b = (_a = hooks.afterOptionsMerged) === null || _a === void 0 ? void 0 : _a.call(hooks, { options })) !== null && _b !== void 0 ? _b : options; }, mergedOptions); } function beforeRequest({ request, options, ...rest }) { var _a; return ((_a = options === null || options === void 0 ? void 0 : options.hooks) !== null && _a !== void 0 ? _a : []).reduce(async (request, hooks) => { var _a, _b; request = await request; const result = await ((_a = hooks.beforeRequest) === null || _a === void 0 ? void 0 : _a.call(hooks, { request, options, ...rest })); if (!result) return request; else if (utils.types.instanceOf(result, Request)) return result; else if (utils.types.has(result, 'url')) return new Request(result.url, (_b = result.request) !== null && _b !== void 0 ? _b : result); else return new Request(result.request, result); }, request); } function beforeRetry({ request, options, ...rest }) { var _a; return ((_a = options === null || options === void 0 ? void 0 : options.hooks) !== null && _a !== void 0 ? _a : []).reduce(async (request, hooks) => { var _a, _b; request = await request; if (request === stop) return request; const result = await ((_a = hooks.beforeRetry) === null || _a === void 0 ? void 0 : _a.call(hooks, { request, options, ...rest })); if (result === stop) return result; else if (!result) return request; else if (utils.types.instanceOf(result, Request)) return result; else if (utils.types.has(result, 'url')) return new Request(result.url, (_b = result.request) !== null && _b !== void 0 ? _b : result); else return new Request(result.request, result); }, request); } function afterResponse({ response, options, ...rest }) { var _a, _b; return (_b = (((_a = options === null || options === void 0 ? void 0 : options.hooks) !== null && _a !== void 0 ? _a : []))) === null || _b === void 0 ? void 0 : _b.reduce(async (response, hooks) => { var _a, _b, _c, _d; response = await response; const result = await ((_a = hooks.afterResponse) === null || _a === void 0 ? void 0 : _a.call(hooks, { response, options, ...rest })); if (!result) return response; else if (utils.types.instanceOf(result, Response)) return result; else return new Response((_b = result.body) !== null && _b !== void 0 ? _b : (_c = result.response) === null || _c === void 0 ? void 0 : _c.body, (_d = result.response) !== null && _d !== void 0 ? _d : result); }, response); } function afterError({ error, options, ...rest }) { var _a, _b; return (_b = (((_a = options === null || options === void 0 ? void 0 : options.hooks) !== null && _a !== void 0 ? _a : []))) === null || _b === void 0 ? void 0 : _b.reduce(async (error, hooks) => { var _a; error = await error; return (await ((_a = hooks.afterError) === null || _a === void 0 ? void 0 : _a.call(hooks, { error, options, ...rest }))) || error; }, error); } function calculateTimeout(requestTimeout, body, options) { var _a; if (!requestTimeout) return requestTimeout; if (utils.types.isNumber(requestTimeout)) return requestTimeout; if (utils.types.isPlainObject(requestTimeout)) { const { base, perByte } = requestTimeout; if (utils.types.isString(body)) return base + perByte * body.length; if (utils.types.isArray(body)) return base + perByte * body.reduce((acc, item) => acc + item.length, 0); if (utils.types.instanceOf(body, ArrayBuffer)) return base + perByte * body.byteLength; if (utils.types.instanceOf(body, Uint8Array) || utils.types.instanceOf(body, Uint16Array) || utils.types.instanceOf(body, Uint32Array)) return base + perByte * body.byteLength; if (global.Blob && utils.types.instanceOf(body, Blob)) return base + perByte * body.size; if (global.Buffer && utils.types.instanceOf(body, Buffer)) return base + perByte * body.byteLength; const allHooks = ((_a = options === null || options === void 0 ? void 0 : options.hooks) !== null && _a !== void 0 ? _a : []); allHooks.forEach(hooks => { var _a; (_a = hooks.unknownBodyType) === null || _a === void 0 ? void 0 : _a.call(hooks, { body, options }); }); return base + perByte * 10; } return requestTimeout; }