UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

180 lines (179 loc) 6.8 kB
/** * HTTP Retry Handler for MCP Transport * * Provides retry logic with exponential backoff and jitter * specifically designed for HTTP-based MCP transport connections. */ import { isAbortError } from "../utils/errorHandling.js"; import { calculateBackoffDelay } from "../utils/retryHandler.js"; import { logger } from "../utils/logger.js"; import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../observability/index.js"; import { getActiveTraceContext } from "../telemetry/traceContext.js"; /** * Default HTTP retry configuration */ export const DEFAULT_HTTP_RETRY_CONFIG = { maxAttempts: 3, initialDelay: 1000, maxDelay: 30000, backoffMultiplier: 2, retryableStatusCodes: [408, 429, 500, 502, 503, 504], }; /** * Sleep utility for retry delays */ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Check if an HTTP status code is retryable based on configuration * * @param status - HTTP status code to check * @param config - HTTP retry configuration * @returns True if the status code should trigger a retry */ export function isRetryableStatusCode(status, config = DEFAULT_HTTP_RETRY_CONFIG) { return config.retryableStatusCodes.includes(status); } /** * Check if an error is retryable for HTTP operations * * Considers: * - Network errors (ECONNRESET, ENOTFOUND, ECONNREFUSED, ETIMEDOUT) * - Timeout errors * - HTTP status codes in the retryable list * - Fetch/network-related errors * * @param error - Error to check * @param config - HTTP retry configuration (optional) * @returns True if the error is retryable */ export function isRetryableHTTPError(error, config = DEFAULT_HTTP_RETRY_CONFIG) { if (!error || typeof error !== "object") { return false; } const errorObj = error; // User-initiated aborts are NOT retryable — the caller explicitly cancelled if (isAbortError(error)) { return false; } // Check for timeout errors if (errorObj.name === "TimeoutError" || errorObj.code === "TIMEOUT" || errorObj.code === "ETIMEDOUT") { return true; } // Check for network-related errors if (errorObj.code === "ECONNRESET" || errorObj.code === "ENOTFOUND" || errorObj.code === "ECONNREFUSED" || errorObj.code === "ECONNABORTED" || errorObj.code === "EPIPE" || errorObj.code === "ENETUNREACH" || errorObj.code === "EHOSTUNREACH") { return true; } // Check for fetch errors (network failures) if (errorObj.name === "TypeError" && typeof errorObj.message === "string") { const message = errorObj.message.toLowerCase(); if (message.includes("fetch") || message.includes("network") || message.includes("connection")) { return true; } } // Check for HTTP status codes if (typeof errorObj.status === "number") { return isRetryableStatusCode(errorObj.status, config); } // Check for response object with status if (errorObj.response && typeof errorObj.response === "object" && typeof errorObj.response.status === "number") { return isRetryableStatusCode(errorObj.response.status, config); } // Check for statusCode (alternative property name) if (typeof errorObj.statusCode === "number") { return isRetryableStatusCode(errorObj.statusCode, config); } return false; } /** * Execute an HTTP operation with retry logic * * Implements exponential backoff with jitter to avoid thundering herd problems. * Uses the calculateBackoffDelay function from the core retry handler for * consistent delay calculation across the codebase. * * @param operation - Async operation to execute with retries * @param config - Partial HTTP retry configuration (merged with defaults) * @returns Result of the operation * @throws Last error if all retry attempts fail * * @example * ```typescript * const result = await withHTTPRetry( * async () => { * const response = await fetch(url); * if (!response.ok) { * const error = new Error(`HTTP ${response.status}`) as Error & { status: number }; * error.status = response.status; * throw error; * } * return response.json(); * }, * { maxAttempts: 5, initialDelay: 500 } * ); * ``` */ export async function withHTTPRetry(operation, config = {}) { const mergedConfig = { ...DEFAULT_HTTP_RETRY_CONFIG, ...config, }; const { traceId, parentSpanId } = getActiveTraceContext(); const span = SpanSerializer.createSpan(SpanType.MCP_TRANSPORT, "mcp.retry", { "mcp.transport": "http", "mcp.operation": "retry", "mcp.maxAttempts": mergedConfig.maxAttempts, }, parentSpanId, traceId); const startTime = Date.now(); let lastError; let actualAttempts = 0; for (let attempt = 1; attempt <= mergedConfig.maxAttempts; attempt++) { actualAttempts = attempt; try { const result = await operation(); span.durationMs = Date.now() - startTime; span.attributes["mcp.retryAttempt"] = attempt; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.OK); getMetricsAggregator().recordSpan(endedSpan); return result; } catch (error) { lastError = error; // Don't retry if it's the last attempt if (attempt === mergedConfig.maxAttempts) { logger.debug(`HTTP retry: All ${mergedConfig.maxAttempts} attempts exhausted`); break; } // Check if we should retry this error if (!isRetryableHTTPError(error, mergedConfig)) { logger.debug(`HTTP retry: Non-retryable error encountered`, error instanceof Error ? error.message : String(error)); break; } // Calculate delay using the shared backoff calculation const delay = calculateBackoffDelay(attempt, mergedConfig.initialDelay, mergedConfig.backoffMultiplier, mergedConfig.maxDelay, true); const errorMessage = error instanceof Error ? error.message : String(error); logger.warn(`HTTP retry: Attempt ${attempt}/${mergedConfig.maxAttempts} failed: ${errorMessage}. Retrying in ${Math.round(delay)}ms...`); await sleep(delay); } } span.durationMs = Date.now() - startTime; span.attributes["mcp.retryAttempt"] = actualAttempts; const endedSpan = SpanSerializer.endSpan(span, SpanStatus.ERROR); endedSpan.statusMessage = lastError instanceof Error ? lastError.message : String(lastError); getMetricsAggregator().recordSpan(endedSpan); throw lastError; }