undici-retry
Version:
Library for handling retry logic with undici HTTP client
155 lines • 6.75 kB
JavaScript
;
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