@vivocha/request-retry
Version:
HTTP requests with retry, and API client utilities
176 lines • 9.15 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeRetryAfter = exports.APIClient = void 0;
const debuggo_1 = require("debuggo");
const rp = require("request-promise-native");
const types_1 = require("./types");
class APIClient {
constructor(baseUrl, logger = (0, debuggo_1.getLogger)('vivocha.api-client')) {
this.baseUrl = baseUrl;
this.logger = logger;
this.DEFAULT_MAX = 10 * 60 * 1000; // default maxRetryAfter is 10 minutes
this.firstCall = true;
}
async call(options) {
var _a, _b, _c, _d;
if (!(options === null || options === void 0 ? void 0 : options.method)) {
options.method = 'get';
}
if (typeof (options === null || options === void 0 ? void 0 : options.path) === 'undefined') {
options.path = '';
}
const apiEndpoint = `${this.baseUrl}${options.path}`;
try {
const requestOpts = {
method: options.method,
headers: options.headers ? options.headers : {},
simple: false,
resolveWithFullResponse: true
};
if (options === null || options === void 0 ? void 0 : options.json) {
requestOpts.json = true;
}
if (options.qs) {
requestOpts.qs = options.qs;
}
if (options.body) {
requestOpts.body = options.body;
}
// * Authentication
if (((_a = options.authOptions) === null || _a === void 0 ? void 0 : _a.token) && ((_b = options.authOptions) === null || _b === void 0 ? void 0 : _b.authorizationType)) {
const authScheme = options.authOptions.authorizationType;
const authType = authScheme === 'bearer' ? authScheme.charAt(0).toUpperCase() + authScheme.slice(1) : authScheme;
requestOpts.headers = Object.assign(Object.assign({}, requestOpts.headers), { authorization: `${authType} ${options.authOptions.token}` });
}
else if (((_c = options.authOptions) === null || _c === void 0 ? void 0 : _c.user) && ((_d = options.authOptions) === null || _d === void 0 ? void 0 : _d.password)) {
requestOpts.auth = {
user: options.authOptions.user,
pass: options.authOptions.password
};
}
// * Request timeout
if (options.timeout) {
requestOpts.timeout = options.timeout;
}
// * Do not retry on errors
options.doNotRetryOnErrors = options.doNotRetryOnErrors && options.doNotRetryOnErrors.length ? options.doNotRetryOnErrors : [];
this.logger.debug(`Calling API endpoint: ${options.method} ${apiEndpoint}`);
const response = await rp(apiEndpoint, requestOpts);
this.logger.debug(`${options.method} ${apiEndpoint} response status`, response.statusCode);
if (response.statusCode >= 200 && response.statusCode <= 299) {
return options.getFullResponse ? response : response.body;
}
else {
this.logger.error(`${options.method} ${apiEndpoint} call returned response error ${response.statusCode}, body: ${JSON.stringify(response.body)}`);
if (options.retries === 0 || this.isDoNotRetryCode(options, response.statusCode)) {
this.logger.warn('No more retries to do, throwing error');
const error = new types_1.APICallError('APICallError', response.body, response.statusCode, `Error calling the API endpoint: ${options.method} ${apiEndpoint}`);
this.logger.error('error', JSON.stringify(error), error.message);
throw error;
}
else {
// * Retrying
// * compute the retry after milliseconds
let computedRetryAfter;
computedRetryAfter =
options.retryAfter ||
(options.minRetryAfter
? this.firstCall
? options.minRetryAfter
: computeRetryAfter(options.minRetryAfter, true, options.maxRetryAfter || this.DEFAULT_MAX)
: 1000);
options.retries = options.retries - 1;
this.firstCall = false;
this.logger.debug(`Going to retry request ${options.method} ${apiEndpoint} in ${computedRetryAfter} ms...`);
options.minRetryAfter = computedRetryAfter;
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve(this.call(options));
}, computedRetryAfter);
});
}
}
}
catch (error) {
this.logger.error(`Error calling endpoint ${options.method} ${apiEndpoint}`);
const apiError = error instanceof types_1.APICallError ? error : new types_1.APICallError(error.name || 'APICallError', null, undefined, error.message);
throw apiError;
}
}
isDoNotRetryCode(options, statusCode) {
var _a, _b, _c;
if (!((_a = options.doNotRetryOnErrors) === null || _a === void 0 ? void 0 : _a.length)) {
return false;
}
else if (options.doNotRetryOnErrors.includes(statusCode) || options.doNotRetryOnErrors.includes(statusCode.toString())) {
return true;
}
else {
//check if there are string patterns
const errorPatterns = options.doNotRetryOnErrors.filter(e => typeof e === 'string');
let isDoNotRetryCode = false;
for (let status of errorPatterns) {
const codexx = /(?<code>\d)xx/;
const codex = /(?<code>\d0)x/;
const xxmatch = codexx.exec(status);
const xmatch = codex.exec(status);
if (xxmatch && ((_b = xxmatch === null || xxmatch === void 0 ? void 0 : xxmatch.groups) === null || _b === void 0 ? void 0 : _b.code) === statusCode.toString().charAt(0)) {
// like 5xx
isDoNotRetryCode = true;
break;
}
else if (xmatch && ((_c = xmatch === null || xmatch === void 0 ? void 0 : xmatch.groups) === null || _c === void 0 ? void 0 : _c.code) && statusCode.toString().startsWith(xmatch.groups.code)) {
// like 40x
isDoNotRetryCode = true;
break;
}
}
return isDoNotRetryCode;
}
}
static async get(url, options = { retries: 2, retryAfter: 1000 }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'get' });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
static async post(url, body, options = { retries: 2, retryAfter: 1000 }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'post', body });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
static async put(url, body, options = { retries: 2, retryAfter: 1000 }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'put', body });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
static async delete(url, options = { retries: 2, retryAfter: 1000 }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'delete' });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
static async patch(url, body, options = { retries: 2, retryAfter: 1000 }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'patch', body });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
static async head(url, options = { retries: 2, retryAfter: 1000, getFullResponse: true }, logger) {
const apiCallOptions = Object.assign(Object.assign({}, options), { path: '', method: 'head', getFullResponse: true });
const client = new APIClient(url, logger);
return client.call(apiCallOptions);
}
}
exports.APIClient = APIClient;
function computeRetryAfter(current, addShift = true, max) {
if (max && current >= max) {
return max;
}
else {
let retryAfterMillis = current * 2;
if (addShift) {
retryAfterMillis = retryAfterMillis + Math.ceil(Math.random() * 50);
}
return max && retryAfterMillis > max ? max : retryAfterMillis;
}
}
exports.computeRetryAfter = computeRetryAfter;
//# sourceMappingURL=request.js.map
;