@applitools/req
Version:
Applitools fetch-based request library
295 lines (294 loc) • 15.8 kB
JavaScript
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;
}