UNPKG

noodle-perplexity-mcp

Version:

A Perplexity API Model Context Protocol (MCP) server that unlocks Perplexity's search-augmented AI capabilities for LLM agents. Features robust error handling, secure input validation, transparent reasoning, and multimodal support with file attachments (P

336 lines (335 loc) 12.8 kB
/** * @fileoverview This module provides utilities for robust error handling. * It defines structures for error context, options for handling errors, * and mappings for classifying errors. The main `ErrorHandler` class * offers static methods for consistent error processing, logging, and transformation. * @module src/utils/internal/errorHandler */ import { BaseErrorCode, McpError } from "../../types-global/errors.js"; import { generateUUID, sanitizeInputForLogging } from "../index.js"; import { logger } from "./logger.js"; /** * Maps standard JavaScript error constructor names to `BaseErrorCode` values. * @private */ const ERROR_TYPE_MAPPINGS = { SyntaxError: BaseErrorCode.VALIDATION_ERROR, TypeError: BaseErrorCode.VALIDATION_ERROR, ReferenceError: BaseErrorCode.INTERNAL_ERROR, RangeError: BaseErrorCode.VALIDATION_ERROR, URIError: BaseErrorCode.VALIDATION_ERROR, EvalError: BaseErrorCode.INTERNAL_ERROR, }; /** * Array of `BaseErrorMapping` rules to classify errors by message/name patterns. * Order matters: more specific patterns should precede generic ones. * @private */ const COMMON_ERROR_PATTERNS = [ { pattern: /auth|unauthorized|unauthenticated|not.*logged.*in|invalid.*token|expired.*token/i, errorCode: BaseErrorCode.UNAUTHORIZED, }, { pattern: /permission|forbidden|access.*denied|not.*allowed/i, errorCode: BaseErrorCode.FORBIDDEN, }, { pattern: /not found|missing|no such|doesn't exist|couldn't find/i, errorCode: BaseErrorCode.NOT_FOUND, }, { pattern: /invalid|validation|malformed|bad request|wrong format|missing required/i, errorCode: BaseErrorCode.VALIDATION_ERROR, }, { pattern: /conflict|already exists|duplicate|unique constraint/i, errorCode: BaseErrorCode.CONFLICT, }, { pattern: /rate limit|too many requests|throttled/i, errorCode: BaseErrorCode.RATE_LIMITED, }, { pattern: /timeout|timed out|deadline exceeded/i, errorCode: BaseErrorCode.TIMEOUT, }, { pattern: /service unavailable|bad gateway|gateway timeout|upstream error/i, errorCode: BaseErrorCode.SERVICE_UNAVAILABLE, }, ]; /** * Creates a "safe" RegExp for testing error messages. * Ensures case-insensitivity and removes the global flag. * @param pattern - The string or RegExp pattern. * @returns A new RegExp instance. * @private */ function createSafeRegex(pattern) { if (pattern instanceof RegExp) { let flags = pattern.flags.replace("g", ""); if (!flags.includes("i")) { flags += "i"; } return new RegExp(pattern.source, flags); } return new RegExp(pattern, "i"); } /** * Retrieves a descriptive name for an error object or value. * @param error - The error object or value. * @returns A string representing the error's name or type. * @private */ function getErrorName(error) { if (error instanceof Error) { return error.name || "Error"; } if (error === null) { return "NullValueEncountered"; } if (error === undefined) { return "UndefinedValueEncountered"; } if (typeof error === "object" && error !== null && error.constructor && typeof error.constructor.name === "string" && error.constructor.name !== "Object") { return `${error.constructor.name}Encountered`; } return `${typeof error}Encountered`; } /** * Extracts a message string from an error object or value. * @param error - The error object or value. * @returns The error message string. * @private */ function getErrorMessage(error) { if (error instanceof Error) { return error.message; } if (error === null) { return "Null value encountered as error"; } if (error === undefined) { return "Undefined value encountered as error"; } if (typeof error === "string") { return error; } try { const str = String(error); if (str === "[object Object]" && error !== null) { try { return `Non-Error object encountered: ${JSON.stringify(error)}`; } catch { return `Unstringifyable non-Error object encountered (constructor: ${error.constructor?.name || "Unknown"})`; } } return str; } catch (e) { return `Error converting error to string: ${e instanceof Error ? e.message : "Unknown conversion error"}`; } } /** * A utility class providing static methods for comprehensive error handling. */ export class ErrorHandler { /** * Determines an appropriate `BaseErrorCode` for a given error. * Checks `McpError` instances, `ERROR_TYPE_MAPPINGS`, and `COMMON_ERROR_PATTERNS`. * Defaults to `BaseErrorCode.INTERNAL_ERROR`. * @param error - The error instance or value to classify. * @returns The determined error code. */ static determineErrorCode(error) { if (error instanceof McpError) { return error.code; } const errorName = getErrorName(error); const errorMessage = getErrorMessage(error); if (errorName in ERROR_TYPE_MAPPINGS) { return ERROR_TYPE_MAPPINGS[errorName]; } for (const mapping of COMMON_ERROR_PATTERNS) { const regex = createSafeRegex(mapping.pattern); if (regex.test(errorMessage) || regex.test(errorName)) { return mapping.errorCode; } } return BaseErrorCode.INTERNAL_ERROR; } /** * Handles an error with consistent logging and optional transformation. * Sanitizes input, determines error code, logs details, and can rethrow. * @param error - The error instance or value that occurred. * @param options - Configuration for handling the error. * @returns The handled (and potentially transformed) error instance. */ static handleError(error, options) { const { context = {}, operation, input, rethrow = false, errorCode: explicitErrorCode, includeStack = true, critical = false, errorMapper, } = options; const sanitizedInput = input !== undefined ? sanitizeInputForLogging(input) : undefined; const originalErrorName = getErrorName(error); const originalErrorMessage = getErrorMessage(error); const originalStack = error instanceof Error ? error.stack : undefined; let finalError; let loggedErrorCode; const errorDetailsSeed = error instanceof McpError && typeof error.details === "object" && error.details !== null ? { ...error.details } : {}; const consolidatedDetails = { ...errorDetailsSeed, ...context, originalErrorName, originalMessage: originalErrorMessage, }; if (originalStack && !(error instanceof McpError && error.details?.originalStack)) { consolidatedDetails.originalStack = originalStack; } if (error instanceof McpError) { loggedErrorCode = error.code; finalError = errorMapper ? errorMapper(error) : new McpError(error.code, error.message, consolidatedDetails); } else { loggedErrorCode = explicitErrorCode || ErrorHandler.determineErrorCode(error); const message = `Error in ${operation}: ${originalErrorMessage}`; finalError = errorMapper ? errorMapper(error) : new McpError(loggedErrorCode, message, consolidatedDetails); } if (finalError !== error && error instanceof Error && finalError instanceof Error && !finalError.stack && error.stack) { finalError.stack = error.stack; } const logRequestId = typeof context.requestId === "string" && context.requestId ? context.requestId : generateUUID(); const logTimestamp = typeof context.timestamp === "string" && context.timestamp ? context.timestamp : new Date().toISOString(); const logPayload = { requestId: logRequestId, timestamp: logTimestamp, operation, input: sanitizedInput, critical, errorCode: loggedErrorCode, originalErrorType: originalErrorName, finalErrorType: getErrorName(finalError), ...Object.fromEntries(Object.entries(context).filter(([key]) => key !== "requestId" && key !== "timestamp")), }; if (finalError instanceof McpError && finalError.details) { logPayload.errorDetails = finalError.details; } else { logPayload.errorDetails = consolidatedDetails; } if (includeStack) { const stack = finalError instanceof Error ? finalError.stack : originalStack; if (stack) { logPayload.stack = stack; } } logger.error(`Error in ${operation}: ${finalError.message || originalErrorMessage}`, logPayload); if (rethrow) { throw finalError; } return finalError; } /** * Maps an error to a specific error type `T` based on `ErrorMapping` rules. * Returns original/default error if no mapping matches. * @template T The target error type, extending `Error`. * @param error - The error instance or value to map. * @param mappings - An array of mapping rules to apply. * @param defaultFactory - Optional factory for a default error if no mapping matches. * @returns The mapped error of type `T`, or the original/defaulted error. */ static mapError(error, mappings, defaultFactory) { const errorMessage = getErrorMessage(error); const errorName = getErrorName(error); for (const mapping of mappings) { const regex = createSafeRegex(mapping.pattern); if (regex.test(errorMessage) || regex.test(errorName)) { return mapping.factory(error, mapping.additionalContext); } } if (defaultFactory) { return defaultFactory(error); } return error instanceof Error ? error : new Error(String(error)); } /** * Formats an error into a consistent object structure for API responses or structured logging. * @param error - The error instance or value to format. * @returns A structured representation of the error. */ static formatError(error) { if (error instanceof McpError) { return { code: error.code, message: error.message, details: typeof error.details === "object" && error.details !== null ? error.details : {}, }; } if (error instanceof Error) { return { code: ErrorHandler.determineErrorCode(error), message: error.message, details: { errorType: error.name || "Error" }, }; } return { code: BaseErrorCode.UNKNOWN_ERROR, message: getErrorMessage(error), details: { errorType: getErrorName(error) }, }; } /** * Safely executes a function (sync or async) and handles errors using `ErrorHandler.handleError`. * The error is always rethrown. * @template T The expected return type of the function `fn`. * @param fn - The function to execute. * @param options - Error handling options (excluding `rethrow`). * @returns A promise resolving with the result of `fn` if successful. * @throws {McpError | Error} The error processed by `ErrorHandler.handleError`. * @example * ```typescript * async function fetchData(userId: string, context: RequestContext) { * return ErrorHandler.tryCatch( * async () => { * const response = await fetch(`/api/users/${userId}`); * if (!response.ok) throw new Error(`Failed to fetch user: ${response.status}`); * return response.json(); * }, * { operation: 'fetchUserData', context, input: { userId } } * ); * } * ``` */ static async tryCatch(fn, options) { try { return await Promise.resolve(fn()); } catch (error) { // ErrorHandler.handleError will return the error to be thrown. throw ErrorHandler.handleError(error, { ...options, rethrow: true }); } } }