/** * Retry Utility * * Provides retry logic with exponential backoff for HTTP requests. */ import { Logger } from "@nestjs/common"; import type { AxiosError } from "axios"; const logger = new Logger("RetryUtil"); /** * Configuration for retry logic */ export interface RetryConfig { /** Maximum number of retry attempts (default: 3) */ maxRetries?: number; /** Initial backoff delay in milliseconds (default: 1000) */ initialDelay?: number; /** Maximum backoff delay in milliseconds (default: 8000) */ maxDelay?: number; /** Backoff multiplier (default: 2 for exponential) */ backoffMultiplier?: number; } /** * Default retry configuration */ const DEFAULT_CONFIG: Required = { maxRetries: 3, initialDelay: 1000, // 1 second maxDelay: 8000, // 8 seconds backoffMultiplier: 2, }; /** * Check if error is retryable (network errors, timeouts, 5xx errors) * Do NOT retry on 4xx errors (client errors) */ export function isRetryableError(error: unknown): boolean { // Check if it's a plain object (for testing) or Error instance if (!error || (typeof error !== "object" && !(error instanceof Error))) { return false; } const axiosError = error as AxiosError; // Retry on network errors (no response received) if (!axiosError.response) { // Check for network error codes const networkErrorCodes = [ "ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ENETUNREACH", "EAI_AGAIN", ]; if (axiosError.code && networkErrorCodes.includes(axiosError.code)) { return true; } // Retry on timeout if (axiosError.message.includes("timeout")) { return true; } return false; } // Retry on 5xx server errors const status = axiosError.response.status; if (status >= 500 && status < 600) { return true; } // Retry on 429 (Too Many Requests) with backoff if (status === 429) { return true; } // Do NOT retry on 4xx client errors return false; } /** * Execute a function with retry logic and exponential backoff */ export async function withRetry( operation: () => Promise, config: RetryConfig = {} ): Promise { const finalConfig: Required = { ...DEFAULT_CONFIG, ...config, }; let lastError: Error | undefined; let delay = finalConfig.initialDelay; for (let attempt = 0; attempt <= finalConfig.maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; // If this is the last attempt, don't retry if (attempt === finalConfig.maxRetries) { break; } // Check if error is retryable if (!isRetryableError(error)) { logger.warn(`Non-retryable error, aborting retry: ${lastError.message}`); throw error; } // Log retry attempt const errorMessage = lastError instanceof Error ? lastError.message : "Unknown error"; logger.warn( `Retry attempt ${String(attempt + 1)}/${String(finalConfig.maxRetries)} after error: ${errorMessage}. Retrying in ${String(delay)}ms...` ); // Wait with exponential backoff await sleep(delay); // Calculate next delay with exponential backoff delay = Math.min(delay * finalConfig.backoffMultiplier, finalConfig.maxDelay); } } // All retries exhausted if (lastError) { throw lastError; } // Should never reach here, but satisfy TypeScript throw new Error("Operation failed after retries"); } /** * Sleep for specified milliseconds */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }