UNPKG

@vivocha/request-retry

Version:

HTTP requests with retry, and API client utilities

176 lines 9.15 kB
"use strict"; 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