/** * Error formatter utility for standardizing error messages across connectors * Extracts human-readable messages from various error types (axios, custom errors, etc.) */ export interface FormattedError { message: string; code: string; httpStatus?: number; metadata?: Record; } /** * Extract meaningful error message from various error types */ export function formatError(error: any): FormattedError { if (!error) { return { message: 'Unknown error occurred', code: 'UNKNOWN_ERROR', }; } // Handle axios errors FIRST (before checking for FormattedError) // because axios errors also have message and code properties if (error.isAxiosError || error.name === 'AxiosError') { return formatAxiosError(error); } // If it's already a FormattedError, return as-is // Check for httpStatus or metadata to distinguish from axios errors if (error.message && error.code && typeof error.message === 'string' && typeof error.code === 'string' && !error.config) { return error as FormattedError; } // Handle standard Error objects if (error instanceof Error) { return { message: error.message || 'An error occurred', code: 'ERROR', metadata: { name: error.name, }, }; } // Handle string errors if (typeof error === 'string') { return { message: error, code: 'ERROR', }; } // Last resort: try to extract any useful info const message = error.message || error.toString?.() || 'Unknown error occurred'; return { message: typeof message === 'string' ? message : JSON.stringify(message), code: error.code || 'UNKNOWN_ERROR', }; } /** * Format axios-specific errors with detailed context */ function formatAxiosError(error: any): FormattedError { const httpStatus = error.response?.status; const method = error.config?.method?.toUpperCase(); const url = error.config?.url; // First check if connector already extracted the message let message = error._extractedMessage; // If not, try to extract from response data if (!message) { const extractedMessage = extractErrorMessage(error.response?.data); message = extractedMessage || error.response?.statusText || error.message || 'Request failed'; } // Determine error code based on status const code = categorizeHttpError(httpStatus); const metadata: Record = { method, url, }; // Add additional context for specific error types if (httpStatus === 401 || httpStatus === 403) { metadata.hint = 'Check authentication credentials'; } else if (httpStatus === 404) { metadata.hint = 'Resource not found - check endpoint URL'; } else if (httpStatus && httpStatus >= 500) { metadata.hint = 'ERP system may be temporarily unavailable'; } return { message, code, httpStatus, metadata, }; } /** * Extract meaningful error message from response data * Handles various ERP-specific response formats */ function extractErrorMessage(data: any): string | null { if (!data) return null; // Common error message fields across different ERPs const possibleFields = [ 'ErrorMessage', // Epicor 'error.message', // Common REST format 'error', // Simple format 'Message', // .NET style 'message', // JavaScript style 'errorMessage', // Camel case 'error_message', // Snake case 'errors[0].message', // Array of errors 'title', // Problem details format 'detail', // Problem details format ]; for (const field of possibleFields) { const value = getNestedValue(data, field); if (value && typeof value === 'string') { return value; } } // If data is a string, return it if (typeof data === 'string') { // Try to parse as JSON first try { const parsed = JSON.parse(data); return extractErrorMessage(parsed); } catch { return data; } } return null; } /** * Get nested value from object using dot notation */ function getNestedValue(obj: any, path: string): any { const parts = path.split('.'); let current = obj; for (const part of parts) { // Handle array notation like 'errors[0]' const arrayMatch = part.match(/(\w+)\[(\d+)\]/); if (arrayMatch) { const [, key, index] = arrayMatch; current = current?.[key]?.[parseInt(index, 10)]; } else { current = current?.[part]; } if (current === undefined || current === null) { return null; } } return current; } /** * Categorize HTTP errors into standard codes */ function categorizeHttpError(status?: number): string { if (!status) return 'NETWORK_ERROR'; if (status === 400) return 'VALIDATION_ERROR'; if (status === 401) return 'AUTHENTICATION_ERROR'; if (status === 403) return 'AUTHORIZATION_ERROR'; if (status === 404) return 'NOT_FOUND'; if (status === 409) return 'CONFLICT'; if (status === 422) return 'VALIDATION_ERROR'; if (status === 429) return 'RATE_LIMIT'; if (status >= 500) return 'ERP_SERVER_ERROR'; if (status >= 400) return 'CLIENT_ERROR'; return 'HTTP_ERROR'; } /** * Format error for logging (shorter, cleaner version) */ export function formatErrorForLogging(error: any): string { const formatted = formatError(error); let message = `[${formatted.code}] ${formatted.message}`; if (formatted.httpStatus) { message = `[${formatted.httpStatus}] ${message}`; } return message; }