/** * Retry Plugin * * Automatically retries failed requests with exponential backoff */ import { defaultLogger } from '../../observability/logger.js'; import type { Plugin, PluginErrorContext, RetryPluginConfig } from '../types.js'; /** * Default configuration */ const DEFAULT_CONFIG: Required = { maxRetries: 3, initialDelay: 1000, // 1 second maxDelay: 30000, // 30 seconds backoffMultiplier: 2, retryableStatusCodes: [429, 502, 503, 504], retryableErrors: [ 'ECONNRESET', 'ENOTFOUND', 'ESOCKETTIMEDOUT', 'ETIMEDOUT', 'ECONNREFUSED', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN', ], }; /** * Create retry plugin */ export function createRetryPlugin(config: RetryPluginConfig = {}): Plugin { const pluginConfig = { ...DEFAULT_CONFIG, ...config }; const logger = defaultLogger; /** * Check if error is retryable */ function isRetryable(context: PluginErrorContext): boolean { // Check if we've exceeded max retries if (context.attempts >= pluginConfig.maxRetries) { return false; } // Check status code if response is available if (context.response) { return pluginConfig.retryableStatusCodes.includes(context.response.statusCode); } // Check error code/type const errorCode = (context.error as any).code; const errorType = context.error.name; if (errorCode && pluginConfig.retryableErrors.includes(errorCode)) { return true; } if (pluginConfig.retryableErrors.includes(errorType)) { return true; } // Check for timeout errors if ( errorType === 'TimeoutError' || context.error.message.toLowerCase().includes('timeout') ) { return true; } // Check for connection errors if ( errorType === 'ConnectionError' || context.error.message.toLowerCase().includes('connection') ) { return true; } return false; } /** * Calculate retry delay with exponential backoff */ function calculateDelay(attempt: number): number { const delay = pluginConfig.initialDelay * Math.pow(pluginConfig.backoffMultiplier, attempt); return Math.min(delay, pluginConfig.maxDelay); } /** * Sleep for specified duration */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } return { name: 'retry', version: '1.0.0', priority: 90, // Execute late, close to the actual request onError: async (context: PluginErrorContext) => { // Check if error is retryable if (!isRetryable(context)) { logger.debug('Error not retryable', { error: context.error.name, attempts: context.attempts, maxRetries: pluginConfig.maxRetries, }); return null; } // Calculate delay const delay = calculateDelay(context.attempts); logger.info('Retrying request', { requestId: context.request.requestId, attempt: context.attempts + 1, maxRetries: pluginConfig.maxRetries, delay, error: context.error.message, statusCode: context.response?.statusCode, }); // Wait before retrying await sleep(delay); // Note: We don't actually retry the request here because we can't // access the client from the plugin. Instead, we return null to // indicate that the error was not handled, and the caller should // handle the retry logic. // // In a real implementation, you would integrate this with the // connection manager to actually retry the request. return null; }, }; }