UNPKG

undici-retry

Version:

Library for handling retry logic with undici HTTP client

155 lines 6.75 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_REQUEST_PARAMS = exports.NO_RETRY_CONFIG = exports.DEFAULT_RETRY_CONFIG = void 0; exports.sendWithRetry = sendWithRetry; const promises_1 = require("node:timers/promises"); const undici_1 = require("undici"); const UndiciRetryRequestError_1 = require("./UndiciRetryRequestError"); const UnprocessableResponseError_1 = require("./UnprocessableResponseError"); const retryAfterResolver_1 = require("./retryAfterResolver"); const typeGuards_1 = require("./typeGuards"); const TIMEOUT_ERRORS = [undici_1.errors.BodyTimeoutError.name, undici_1.errors.HeadersTimeoutError.name]; exports.DEFAULT_RETRY_CONFIG = { maxAttempts: 3, delayBetweenAttemptsInMsecs: 100, statusCodesToRetry: [429, 500, 502, 503, 504], retryOnTimeout: false, respectRetryAfter: true, maxRetryAfterInMsecs: 60000, }; exports.NO_RETRY_CONFIG = { maxAttempts: 1, delayBetweenAttemptsInMsecs: 0, statusCodesToRetry: [], respectRetryAfter: false, retryOnTimeout: false, }; exports.DEFAULT_REQUEST_PARAMS = { blobBody: false, safeParseJson: false, }; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation> async function sendWithRetry(client, request, retryConfig = exports.DEFAULT_RETRY_CONFIG, requestParams = exports.DEFAULT_REQUEST_PARAMS) { let attemptsSoFar = 0; while (true) { attemptsSoFar++; try { const response = await client.request(request); // success if (response.statusCode < 400) { const resolvedBody = await resolveBody(response, requestParams.requestLabel, requestParams.blobBody, requestParams.safeParseJson); return { result: { body: resolvedBody, headers: response.headers, statusCode: response.statusCode, }, }; } // Do not retry, return last error response if (retryConfig.statusCodesToRetry.indexOf(response.statusCode) === -1 || attemptsSoFar >= retryConfig.maxAttempts) { const resolvedBody = await resolveBody(response, requestParams.requestLabel); return { error: { body: resolvedBody, headers: response.headers, statusCode: response.statusCode, requestLabel: requestParams.requestLabel, }, }; } if (retryConfig.delayBetweenAttemptsInMsecs || retryConfig.delayResolver || response.statusCode === 429) { let delay; // TOO_MANY_REQUESTS if (retryConfig.respectRetryAfter !== false && response.statusCode === 429 && 'retry-after' in response.headers) { const delayResolutionResult = (0, retryAfterResolver_1.resolveDelayTime)(response.headers, retryConfig.maxRetryAfterInMsecs); if (delayResolutionResult.result) { delay = delayResolutionResult.result; } if (delayResolutionResult.error === 'max_delay_exceeded') { delay = -1; } } if (delay === undefined) { delay = retryConfig.delayResolver ? (retryConfig.delayResolver(response) ?? retryConfig.delayBetweenAttemptsInMsecs ?? 0) : retryConfig.delayBetweenAttemptsInMsecs; } // Do not retry if (delay === -1) { const resolvedBody = await resolveBody(response, requestParams.requestLabel); return { error: { body: resolvedBody, headers: response.headers, statusCode: response.statusCode, requestLabel: requestParams.requestLabel, }, }; } // retry // undici response body always has to be processed or discarded await response.body.dump(); await (0, promises_1.setTimeout)(delay); } else { await response.body.dump(); } // biome-ignore lint/suspicious/noExplicitAny: <explanation> } catch (err) { // on internal client error we can't do much; if there are still retries left, we retry, if not, we rethrow an error if (attemptsSoFar >= retryConfig.maxAttempts || (retryConfig.retryOnTimeout === false && TIMEOUT_ERRORS.indexOf(err.name) !== -1)) { if (!requestParams.throwOnInternalError) { if (!(0, typeGuards_1.isUnprocessableResponseError)(err)) { err.requestLabel = requestParams.requestLabel; err.isInternalRequestError = true; } return { error: err, }; } if ((0, typeGuards_1.isUnprocessableResponseError)(err)) { throw err; } throw new UndiciRetryRequestError_1.UndiciRetryRequestError({ error: err, message: err.message, requestLabel: requestParams.requestLabel, }); } } } } async function resolveBody(response, requestLabel = 'N/A', blobBody = false, safeParseJson = false) { if (blobBody) { return await response.body.blob(); } // There can never be multiple content-type headers, see https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2 const contentType = response.headers['content-type']; if (contentType?.startsWith('application/json')) { if (!safeParseJson) { return await response.body.json(); } const rawBody = await response.body.text(); try { return JSON.parse(rawBody); } catch (_err) { throw new UnprocessableResponseError_1.UnprocessableResponseError({ message: 'Error while parsing HTTP JSON response', errorCode: 'INVALID_HTTP_RESPONSE_JSON', requestLabel, rawBody, }); } } return await response.body.text(); } //# sourceMappingURL=undiciRetry.js.map