@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
387 lines (386 loc) • 13.2 kB
JavaScript
/**
* File Processing Error Helpers
*
* Utilities for creating consistent, user-friendly file processing errors.
* Provides factory functions for structured errors, retry determination logic,
* and HTTP status extraction.
*
* @module processors/errors
*/
import { isAbortError } from "../../utils/errorHandling.js";
import { ERROR_MESSAGES, FileErrorCode } from "./FileErrorCode.js";
/**
* Create a structured file processing error with user-friendly messaging.
*
* @param code - The error code from FileErrorCode enum
* @param details - Additional context for the error (e.g., file size, format)
* @param originalError - The original error that caused this failure
* @returns A structured FileProcessingError with user-friendly messaging
*
* @example
* ```typescript
* const error = createFileError(FileErrorCode.FILE_TOO_LARGE, {
* sizeMB: "15.5",
* maxMB: "10",
* filename: "large-document.pdf",
* });
* ```
*/
export function createFileError(code, details, originalError) {
const template = ERROR_MESSAGES[code];
const result = {
code,
message: template.message,
userMessage: template.userMessage,
suggestedAction: template.suggestedAction,
retryable: template.retryable,
};
// Add details if provided
if (details && Object.keys(details).length > 0) {
result.details = details;
}
// Add technical details from original error
const technicalDetails = originalError?.message || details?.technicalDetails;
if (technicalDetails) {
result.technicalDetails = technicalDetails;
}
if (originalError) {
result.originalError = originalError;
}
return result;
}
/**
* Create a file processing error with custom messages.
* Useful when you need to override the default messages with context-specific ones.
*
* @param code - The error code from FileErrorCode enum
* @param customMessage - Custom technical message
* @param customUserMessage - Custom user-friendly message
* @param details - Additional context
* @returns A structured FileProcessingError
*/
export function createCustomFileError(code, customMessage, customUserMessage, details) {
const template = ERROR_MESSAGES[code];
return {
code,
message: customMessage,
userMessage: customUserMessage,
suggestedAction: template.suggestedAction,
retryable: template.retryable,
details,
};
}
/**
* Extract HTTP status code from various error types.
*
* @param error - The error to extract status from
* @returns The HTTP status code if found, null otherwise
*
* @example
* ```typescript
* const status = extractHttpStatus(error);
* if (status === 404) {
* // Handle not found
* }
* ```
*/
export function extractHttpStatus(error) {
if (!error) {
return null;
}
// Check for Error objects with HTTP status in message
if (error instanceof Error) {
// Match patterns like "HTTP 404", "status: 500", "statusCode: 503"
const patterns = [
/HTTP\s*(\d{3})/i,
/status[:\s]+(\d{3})/i,
/statusCode[:\s]+(\d{3})/i,
/\b(\d{3})\s+(?:Not Found|Unauthorized|Forbidden|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)/i,
];
for (const pattern of patterns) {
const match = error.message.match(pattern);
if (match?.[1]) {
return parseInt(match[1], 10);
}
}
}
// Check for objects with status or statusCode property
if (typeof error === "object" && error !== null) {
const errorObj = error;
if (typeof errorObj.status === "number") {
return errorObj.status;
}
if (typeof errorObj.statusCode === "number") {
return errorObj.statusCode;
}
if (typeof errorObj.response === "object" && errorObj.response !== null) {
const response = errorObj.response;
if (typeof response.status === "number") {
return response.status;
}
}
}
return null;
}
/**
* Determine if an error is retryable based on error type and characteristics.
* Checks for transient errors like 5xx, network errors, and timeouts.
*
* @param error - The error to check
* @returns true if the error is transient and potentially retryable
*
* @example
* ```typescript
* if (isRetryableError(error) && retryCount < maxRetries) {
* await delay(backoffMs);
* return retry();
* }
* ```
*/
export function isRetryableError(error) {
// Check FileProcessingError
if (isFileProcessingError(error)) {
return error.retryable ?? false;
}
if (error instanceof Error) {
// User-initiated aborts are NOT retryable
if (isAbortError(error)) {
return false;
}
// Network/timeout errors are retryable
const retryableNames = ["TimeoutError", "FetchError"];
if (retryableNames.includes(error.name)) {
return true;
}
const message = error.message.toLowerCase();
// HTTP 5xx server errors are retryable
if (/http\s*5\d{2}/i.test(error.message)) {
return true;
}
// HTTP 429 (rate limiting) is retryable
if (/http\s*429/i.test(error.message) || message.includes("rate limit")) {
return true;
}
// HTTP 408 (request timeout) is retryable
if (/http\s*408/i.test(error.message)) {
return true;
}
// Connection errors are retryable
const connectionPatterns = [
"econnreset",
"etimedout",
"econnrefused",
"enotfound",
"enetunreach",
"ehostunreach",
"epipe",
"socket hang up",
"network error",
"connection refused",
"connection reset",
"dns lookup failed",
];
if (connectionPatterns.some((pattern) => message.includes(pattern))) {
return true;
}
// Temporary/transient errors are retryable
if (message.includes("temporary") || message.includes("try again")) {
return true;
}
}
// Check HTTP status from error object
const status = extractHttpStatus(error);
if (status !== null) {
// 5xx errors (server errors) are retryable
if (status >= 500 && status < 600) {
return true;
}
// 408 (Request Timeout) and 429 (Too Many Requests) are retryable
if (status === 408 || status === 429) {
return true;
}
}
return false;
}
/**
* Type guard to check if an error is a FileProcessingError.
*
* @param error - The value to check
* @returns true if error is a FileProcessingError
*/
export function isFileProcessingError(error) {
if (typeof error !== "object" || error === null) {
return false;
}
const e = error;
return (typeof e.code === "string" &&
typeof e.message === "string" &&
typeof e.userMessage === "string" &&
typeof e.retryable === "boolean");
}
/**
* Map an error to the appropriate FileErrorCode based on its characteristics.
*
* @param error - The error to analyze
* @returns The most appropriate FileErrorCode
*/
export function mapErrorToCode(error) {
if (!error) {
return FileErrorCode.UNKNOWN_ERROR;
}
// Check HTTP status first
const status = extractHttpStatus(error);
if (status !== null) {
if (status === 401 || status === 403) {
return FileErrorCode.DOWNLOAD_AUTH_FAILED;
}
if (status === 404) {
return FileErrorCode.FILE_NOT_FOUND;
}
if (status === 408) {
return FileErrorCode.DOWNLOAD_TIMEOUT;
}
if (status === 429) {
return FileErrorCode.RATE_LIMITED;
}
if (status >= 500) {
return FileErrorCode.DOWNLOAD_FAILED;
}
}
if (error instanceof Error) {
const message = error.message.toLowerCase();
const name = error.name;
// User-initiated aborts are not timeouts — map to NETWORK_ERROR
// (no dedicated ABORTED code exists in FileErrorCode)
if (isAbortError(error)) {
return FileErrorCode.NETWORK_ERROR;
}
// Timeout errors
if (name === "TimeoutError" || message.includes("timeout")) {
return FileErrorCode.DOWNLOAD_TIMEOUT;
}
// Network errors
if (message.includes("econnreset") ||
message.includes("econnrefused") ||
message.includes("network") ||
message.includes("socket")) {
return FileErrorCode.NETWORK_ERROR;
}
// Size errors
if (message.includes("too large") || message.includes("size limit")) {
return FileErrorCode.FILE_TOO_LARGE;
}
// Format/type errors
if (message.includes("unsupported") || message.includes("not supported")) {
return FileErrorCode.UNSUPPORTED_TYPE;
}
if (message.includes("invalid format") || message.includes("malformed")) {
return FileErrorCode.INVALID_FORMAT;
}
if (message.includes("corrupt")) {
return FileErrorCode.CORRUPTED_FILE;
}
// Parsing errors
if (message.includes("parse") || message.includes("syntax")) {
return FileErrorCode.PARSING_FAILED;
}
// Encoding errors
if (message.includes("encoding") ||
message.includes("utf") ||
message.includes("decode")) {
return FileErrorCode.ENCODING_ERROR;
}
// Security errors
if (message.includes("xxe") || message.includes("doctype")) {
return FileErrorCode.XXE_DETECTED;
}
if (message.includes("xss") || message.includes("script")) {
return FileErrorCode.XSS_DETECTED;
}
if (message.includes("zip bomb") || message.includes("compression ratio")) {
return FileErrorCode.ZIP_BOMB_DETECTED;
}
}
return FileErrorCode.UNKNOWN_ERROR;
}
/**
* Format a file processing error for display to users.
*
* @param error - The FileProcessingError to format
* @returns A formatted string suitable for display
*/
export function formatFileError(error) {
let message = error.userMessage;
if (error.suggestedAction) {
message += `\n${error.suggestedAction}`;
}
return message;
}
/**
* Create a processing summary from arrays of results.
*
* @param totalFiles - Total number of files attempted
* @param processedFiles - Successfully processed files
* @param failedFiles - Files that failed to process
* @param skippedFiles - Files that were skipped
* @param warnings - Non-fatal warnings
* @returns A FileProcessingSummary object
*/
export function createProcessingSummary(totalFiles, processedFiles = [], failedFiles = [], skippedFiles = [], warnings = []) {
return {
totalFiles,
processedFiles,
failedFiles,
skippedFiles,
warnings,
};
}
/**
* Combine multiple processing summaries into one.
* Useful when processing different file types separately.
*
* @param summaries - Array of summaries to combine
* @returns A combined FileProcessingSummary
*/
export function combineSummaries(summaries) {
return summaries.reduce((combined, summary) => ({
totalFiles: combined.totalFiles + summary.totalFiles,
processedFiles: [...combined.processedFiles, ...summary.processedFiles],
failedFiles: [...combined.failedFiles, ...summary.failedFiles],
skippedFiles: [...combined.skippedFiles, ...summary.skippedFiles],
warnings: [...combined.warnings, ...summary.warnings],
}), createProcessingSummary(0));
}
/**
* Get retry delay based on error type and attempt number.
* Implements exponential backoff with jitter.
*
* @param error - The error that occurred
* @param attempt - Current attempt number (1-based)
* @param baseDelayMs - Base delay in milliseconds (default: 1000)
* @returns Delay in milliseconds before next retry
*/
export function getRetryDelay(error, attempt, baseDelayMs = 1000) {
// Check for rate limit with Retry-After header
if (typeof error === "object" && error !== null) {
const errorObj = error;
const retryAfter = (errorObj.retryAfter ??
errorObj["retry-after"]);
if (typeof retryAfter === "number") {
return retryAfter * 1000;
}
if (typeof retryAfter === "string") {
const parsed = Number(retryAfter);
if (!Number.isNaN(parsed)) {
return parsed * 1000;
}
}
}
// Exponential backoff: base * 2^(attempt-1)
const safeAttempt = Math.max(1, attempt);
const exponentialDelay = baseDelayMs * 2 ** (safeAttempt - 1);
// Add jitter (0-25% of delay)
const jitter = exponentialDelay * 0.25 * Math.random();
// Cap at 30 seconds
return Math.min(exponentialDelay + jitter, 30000);
}