import { HttpRequest, HttpResponse } from "@smithy/protocol-http"; import { isThrottlingError } from "@smithy/service-error-classification"; import { DEFAULT_MAX_ATTEMPTS, DEFAULT_RETRY_DELAY_BASE, INITIAL_RETRY_TOKENS, INVOCATION_ID_HEADER, REQUEST_HEADER, RETRY_MODES, THROTTLING_RETRY_DELAY_BASE, } from "@smithy/util-retry"; import { v4 } from "uuid"; import { getDefaultRetryQuota } from "./defaultRetryQuota"; import { defaultDelayDecider } from "./delayDecider"; import { defaultRetryDecider } from "./retryDecider"; import { asSdkError } from "./util"; export class StandardRetryStrategy { constructor(maxAttemptsProvider, options) { this.maxAttemptsProvider = maxAttemptsProvider; this.mode = RETRY_MODES.STANDARD; this.retryDecider = options?.retryDecider ?? defaultRetryDecider; this.delayDecider = options?.delayDecider ?? defaultDelayDecider; this.retryQuota = options?.retryQuota ?? getDefaultRetryQuota(INITIAL_RETRY_TOKENS); } shouldRetry(error, attempts, maxAttempts) { return attempts < maxAttempts && this.retryDecider(error) && this.retryQuota.hasRetryTokens(error); } async getMaxAttempts() { let maxAttempts; try { maxAttempts = await this.maxAttemptsProvider(); } catch (error) { maxAttempts = DEFAULT_MAX_ATTEMPTS; } return maxAttempts; } async retry(next, args, options) { let retryTokenAmount; let attempts = 0; let totalDelay = 0; const maxAttempts = await this.getMaxAttempts(); const { request } = args; if (HttpRequest.isInstance(request)) { request.headers[INVOCATION_ID_HEADER] = v4(); } while (true) { try { if (HttpRequest.isInstance(request)) { request.headers[REQUEST_HEADER] = `attempt=${attempts + 1}; max=${maxAttempts}`; } if (options?.beforeRequest) { await options.beforeRequest(); } const { response, output } = await next(args); if (options?.afterRequest) { options.afterRequest(response); } this.retryQuota.releaseRetryTokens(retryTokenAmount); output.$metadata.attempts = attempts + 1; output.$metadata.totalRetryDelay = totalDelay; return { response, output }; } catch (e) { const err = asSdkError(e); attempts++; if (this.shouldRetry(err, attempts, maxAttempts)) { retryTokenAmount = this.retryQuota.retrieveRetryTokens(err); const delayFromDecider = this.delayDecider(isThrottlingError(err) ? THROTTLING_RETRY_DELAY_BASE : DEFAULT_RETRY_DELAY_BASE, attempts); const delayFromResponse = getDelayFromRetryAfterHeader(err.$response); const delay = Math.max(delayFromResponse || 0, delayFromDecider); totalDelay += delay; await new Promise((resolve) => setTimeout(resolve, delay)); continue; } if (!err.$metadata) { err.$metadata = {}; } err.$metadata.attempts = attempts; err.$metadata.totalRetryDelay = totalDelay; throw err; } } } } const getDelayFromRetryAfterHeader = (response) => { if (!HttpResponse.isInstance(response)) return; const retryAfterHeaderName = Object.keys(response.headers).find((key) => key.toLowerCase() === "retry-after"); if (!retryAfterHeaderName) return; const retryAfter = response.headers[retryAfterHeaderName]; const retryAfterSeconds = Number(retryAfter); if (!Number.isNaN(retryAfterSeconds)) return retryAfterSeconds * 1000; const retryAfterDate = new Date(retryAfter); return retryAfterDate.getTime() - Date.now(); };