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
JavaScript
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);
}