UNPKG

lokalise-mcp

Version:

The Lokalise MCP Server brings Lokalise's localization power to Claude and AI assistants—manage projects, keys, and translations by chat.

240 lines (239 loc) • 10.1 kB
import { createApiError } from "./error.util.js"; import { Logger } from "./logger.util.js"; /** * Standard error codes for consistent handling */ export var ErrorCode; (function (ErrorCode) { ErrorCode["NOT_FOUND"] = "NOT_FOUND"; ErrorCode["INVALID_CURSOR"] = "INVALID_CURSOR"; ErrorCode["ACCESS_DENIED"] = "ACCESS_DENIED"; ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR"; ErrorCode["UNEXPECTED_ERROR"] = "UNEXPECTED_ERROR"; ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR"; ErrorCode["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR"; ErrorCode["PRIVATE_IP_ERROR"] = "PRIVATE_IP_ERROR"; ErrorCode["RESERVED_RANGE_ERROR"] = "RESERVED_RANGE_ERROR"; })(ErrorCode || (ErrorCode = {})); /** * Helper function to create a consistent error context object * @param entityType Type of entity being processed * @param operation Operation being performed * @param source Source of the error (typically file path and function) * @param entityId Optional identifier of the entity * @param additionalInfo Optional additional information for debugging * @returns A formatted ErrorContext object */ export function buildErrorContext(entityType, operation, source, entityId, additionalInfo) { return { entityType, operation, source, ...(entityId && { entityId }), ...(additionalInfo && { additionalInfo }), }; } /** * Detect specific error types from raw errors * @param error The error to analyze * @param context Context information for better error detection * @returns Object containing the error code and status code */ export function detectErrorType(error, context = {}) { const methodLogger = Logger.forContext("utils/error-handler.util.ts", "detectErrorType"); methodLogger.debug("Detecting error type", { error, context }); const errorMessage = error instanceof Error ? error.message : String(error); const statusCode = error instanceof Error && "statusCode" in error ? error.statusCode : undefined; // Network error detection if (errorMessage.includes("network error") || errorMessage.includes("fetch failed") || errorMessage.includes("ECONNREFUSED") || errorMessage.includes("ENOTFOUND") || errorMessage.includes("Failed to fetch") || errorMessage.includes("Network request failed")) { return { code: ErrorCode.NETWORK_ERROR, statusCode: 500 }; } // Rate limiting detection if (errorMessage.includes("rate limit") || errorMessage.includes("too many requests") || statusCode === 429) { return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429 }; } // ip-api.com specific error detection if (errorMessage.includes("private range") || errorMessage.includes("private IP")) { return { code: ErrorCode.PRIVATE_IP_ERROR, statusCode: 400 }; } if (errorMessage.includes("reserved range")) { return { code: ErrorCode.RESERVED_RANGE_ERROR, statusCode: 400 }; } // Check for ip-api.com status="fail" in originalError if (error instanceof Error && "originalError" in error && error.originalError && typeof error.originalError === "object") { const originalError = error.originalError; if (originalError.status === "fail") { const apiMessage = originalError.message ? String(originalError.message) : ""; if (apiMessage.includes("private")) { return { code: ErrorCode.PRIVATE_IP_ERROR, statusCode: 400 }; } if (apiMessage.includes("reserved")) { return { code: ErrorCode.RESERVED_RANGE_ERROR, statusCode: 400, }; } return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400 }; } } // Not Found detection if (errorMessage.includes("not found") || errorMessage.includes("does not exist") || statusCode === 404) { return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; } // Access Denied detection if (errorMessage.includes("access") || errorMessage.includes("permission") || errorMessage.includes("authorize") || errorMessage.includes("authentication") || statusCode === 401 || statusCode === 403) { return { code: ErrorCode.ACCESS_DENIED, statusCode: statusCode || 403 }; } // Invalid Cursor detection if ((errorMessage.includes("cursor") || errorMessage.includes("startAt") || errorMessage.includes("page")) && (errorMessage.includes("invalid") || errorMessage.includes("not valid"))) { return { code: ErrorCode.INVALID_CURSOR, statusCode: 400 }; } // Validation Error detection if (errorMessage.includes("validation") || errorMessage.includes("invalid") || errorMessage.includes("required") || statusCode === 400 || statusCode === 422) { return { code: ErrorCode.VALIDATION_ERROR, statusCode: statusCode || 400, }; } // Default to unexpected error return { code: ErrorCode.UNEXPECTED_ERROR, statusCode: statusCode || 500, }; } /** * Create user-friendly error messages based on error type and context * @param code The error code * @param context Context information for better error messages * @param originalMessage The original error message * @returns User-friendly error message */ export function createUserFriendlyErrorMessage(code, context = {}, originalMessage) { const methodLogger = Logger.forContext("utils/error-handler.util.ts", "createUserFriendlyErrorMessage"); const { entityType, entityId, operation } = context; // Format entity ID for display let entityIdStr = ""; if (entityId) { if (typeof entityId === "string") { entityIdStr = entityId; } else { // Handle object entityId entityIdStr = Object.values(entityId).join("/"); } } // Determine entity display name const entity = entityType ? `${entityType}${entityIdStr ? ` ${entityIdStr}` : ""}` : "Resource"; let message = ""; switch (code) { case ErrorCode.NOT_FOUND: message = `${entity} not found${entityIdStr ? `: ${entityIdStr}` : ""}. Verify the ID is correct and that you have access to this ${entityType?.toLowerCase() || "resource"}.`; break; case ErrorCode.ACCESS_DENIED: message = `Access denied for ${entity.toLowerCase()}${entityIdStr ? ` ${entityIdStr}` : ""}. Verify your credentials and permissions.`; break; case ErrorCode.INVALID_CURSOR: message = "Invalid pagination cursor. Use the exact cursor string returned from previous results."; break; case ErrorCode.VALIDATION_ERROR: message = originalMessage || `Invalid data provided for ${operation || "operation"} ${entity.toLowerCase()}.`; break; case ErrorCode.NETWORK_ERROR: message = `Network error while ${operation || "connecting to"} the service. Please check your internet connection and try again.`; break; case ErrorCode.RATE_LIMIT_ERROR: message = "Rate limit exceeded. Please wait a moment and try again, or reduce the frequency of requests."; break; case ErrorCode.PRIVATE_IP_ERROR: message = "Private IP addresses are not supported. Please provide a public IP address."; break; case ErrorCode.RESERVED_RANGE_ERROR: message = "Reserved range IP addresses are not supported. Please provide a public IP address."; break; default: message = `An unexpected error occurred while ${operation || "processing"} ${entity.toLowerCase()}.`; } // Include original message details if available and appropriate if (originalMessage && code !== ErrorCode.NOT_FOUND && code !== ErrorCode.ACCESS_DENIED) { message += ` Error details: ${originalMessage}`; } methodLogger.debug(`Created user-friendly message: ${message}`, { code, context, }); return message; } /** * Handle controller errors consistently * @param error The error to handle * @param context Context information for better error messages * @returns Never returns, always throws an error */ export function handleControllerError(error, context = {}) { const methodLogger = Logger.forContext("utils/error-handler.util.ts", "handleControllerError"); // Extract error details const errorMessage = error instanceof Error ? error.message : String(error); const statusCode = error instanceof Error && "statusCode" in error ? error.statusCode : undefined; // Detect error type using utility const { code, statusCode: detectedStatus } = detectErrorType(error, context); // Combine detected status with explicit status const finalStatusCode = statusCode || detectedStatus; // Format entity information for logging const { entityType, entityId, operation } = context; const entity = entityType || "resource"; const entityIdStr = entityId ? typeof entityId === "string" ? entityId : JSON.stringify(entityId) : ""; const actionStr = operation || "processing"; // Log detailed error information methodLogger.error(`Error ${actionStr} ${entity}${entityIdStr ? `: ${entityIdStr}` : ""}: ${errorMessage}`, error); // Create user-friendly error message for the response const message = code === ErrorCode.VALIDATION_ERROR ? errorMessage : createUserFriendlyErrorMessage(code, context, errorMessage); // Throw an appropriate API error with the user-friendly message throw createApiError(message, finalStatusCode, error); }