error-handler.ts•6.09 kB
/**
* Error Handling Utilities
*
* Provides robust error handling, retry logic, and error formatting
* for ByteBot API interactions.
*/
import { AxiosError } from 'axios';
import { ByteBotError, ErrorCode } from '../types/bytebot.js';
import { RetryConfig } from '../types/mcp.js';
/**
* Custom error class for ByteBot operations
*/
export class ByteBotAPIError extends Error {
constructor(
message: string,
public statusCode: number,
public details?: unknown
) {
super(message);
this.name = 'ByteBotAPIError';
Object.setPrototypeOf(this, ByteBotAPIError.prototype);
}
}
/**
* Convert Axios error to ByteBotAPIError
*/
export function handleAxiosError(error: unknown): ByteBotAPIError {
if (error instanceof AxiosError) {
const statusCode = error.response?.status || ErrorCode.INTERNAL_ERROR;
const errorData = error.response?.data as ByteBotError | undefined;
// Handle different error scenarios
if (error.code === 'ECONNREFUSED') {
return new ByteBotAPIError(
'Cannot connect to ByteBot server. Please ensure ByteBot is running and the endpoint URL is correct.',
ErrorCode.SERVICE_UNAVAILABLE,
{ endpoint: error.config?.baseURL }
);
}
if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
return new ByteBotAPIError(
'Request to ByteBot timed out. The operation took too long to complete.',
ErrorCode.TIMEOUT,
{ timeout: error.config?.timeout }
);
}
// Use error message from API response if available
const message =
errorData?.message ||
error.message ||
'An error occurred while communicating with ByteBot';
return new ByteBotAPIError(message, statusCode, errorData?.details);
}
// Handle non-Axios errors
if (error instanceof Error) {
return new ByteBotAPIError(error.message, ErrorCode.INTERNAL_ERROR);
}
return new ByteBotAPIError(
'An unknown error occurred',
ErrorCode.INTERNAL_ERROR
);
}
/**
* Retry a function with exponential backoff
*/
export async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error | undefined;
let delay = config.delay;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Don't retry on certain error types
if (
error instanceof ByteBotAPIError &&
(error.statusCode === ErrorCode.BAD_REQUEST ||
error.statusCode === ErrorCode.NOT_FOUND)
) {
throw error;
}
// Last attempt - throw the error
if (attempt === config.maxRetries) {
break;
}
// Wait before retrying
console.error(
`[ByteBot MCP] Attempt ${attempt + 1}/${config.maxRetries + 1} failed:`,
lastError.message
);
console.error(`[ByteBot MCP] Retrying in ${delay}ms...`);
await sleep(delay);
// Exponential backoff
if (config.backoffMultiplier) {
delay = Math.min(
delay * config.backoffMultiplier,
config.maxDelay || Infinity
);
}
}
}
throw lastError;
}
/**
* Sleep for specified milliseconds
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Format error for MCP response
*/
export function formatErrorForMCP(error: unknown): {
error: string;
details?: string;
} {
if (error instanceof ByteBotAPIError) {
return {
error: error.message,
details: error.details ? JSON.stringify(error.details, null, 2) : undefined,
};
}
if (error instanceof Error) {
return {
error: error.message,
details: error.stack,
};
}
return {
error: 'An unknown error occurred',
details: String(error),
};
}
/**
* Validate ByteBot endpoint URL
*/
export function validateEndpoint(url: string, name: string): void {
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(
`Invalid ${name} URL: Protocol must be http or https (got ${parsed.protocol})`
);
}
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Invalid ${name} URL: ${url}`);
}
throw error;
}
}
/**
* Validate WebSocket endpoint URL
*/
export function validateWebSocketEndpoint(url: string): void {
try {
const parsed = new URL(url);
if (!['ws:', 'wss:'].includes(parsed.protocol)) {
throw new Error(
`Invalid WebSocket URL: Protocol must be ws or wss (got ${parsed.protocol})`
);
}
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Invalid WebSocket URL: ${url}`);
}
throw error;
}
}
/**
* Log error with context
*/
export function logError(context: string, error: unknown): void {
console.error(`[ByteBot MCP] Error in ${context}:`);
if (error instanceof ByteBotAPIError) {
console.error(` Status: ${error.statusCode}`);
console.error(` Message: ${error.message}`);
if (error.details) {
console.error(` Details:`, error.details);
}
} else if (error instanceof Error) {
console.error(` ${error.message}`);
if (error.stack) {
console.error(` Stack:`, error.stack);
}
} else {
console.error(` ${String(error)}`);
}
}
/**
* Check if error is retryable
*/
export function isRetryableError(error: unknown): boolean {
if (error instanceof ByteBotAPIError) {
// Retry on server errors and timeouts
return (
error.statusCode >= 500 ||
error.statusCode === ErrorCode.TIMEOUT ||
error.statusCode === ErrorCode.SERVICE_UNAVAILABLE
);
}
if (error instanceof AxiosError) {
// Retry on network errors
return (
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT' ||
error.code === 'ECONNABORTED' ||
error.code === 'ENOTFOUND'
);
}
return false;
}