Add retry capability with exponential backoff for HTTP requests. - Implement withRetry utility with configurable retry logic - Exponential backoff: 1s, 2s, 4s, 8s (max) - Maximum 3 retries by default - Retry on network errors (ECONNREFUSED, ETIMEDOUT, etc.) - Retry on 5xx server errors and 429 rate limit - Do NOT retry on 4xx client errors - Integrate with connection service for HTTP requests Fixes #293 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
147 lines
3.6 KiB
TypeScript
147 lines
3.6 KiB
TypeScript
/**
|
|
* 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<RetryConfig> = {
|
|
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<T>(
|
|
operation: () => Promise<T>,
|
|
config: RetryConfig = {}
|
|
): Promise<T> {
|
|
const finalConfig: Required<RetryConfig> = {
|
|
...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<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|