UNPKG

webssh2-server

Version:

A Websocket to SSH2 gateway using xterm.js, socket.io, ssh2

320 lines (319 loc) 9.5 kB
// app/utils/error-handler.ts // Centralized error handling utilities import { err, ok } from './result.js'; import { createNamespacedDebug } from '../logger.js'; const debug = createNamespacedDebug('error-handler'); /** * Standard error types */ export var ErrorType; (function (ErrorType) { ErrorType["Validation"] = "validation"; ErrorType["Authentication"] = "authentication"; ErrorType["Network"] = "network"; ErrorType["Timeout"] = "timeout"; ErrorType["Permission"] = "permission"; ErrorType["Configuration"] = "configuration"; ErrorType["NotFound"] = "not-found"; ErrorType["Conflict"] = "conflict"; ErrorType["RateLimit"] = "rate-limit"; ErrorType["Internal"] = "internal"; ErrorType["Unknown"] = "unknown"; })(ErrorType || (ErrorType = {})); /** * Create a typed error */ export function createTypedError(message, type = ErrorType.Unknown, options) { const error = new Error(message); Object.assign(error, { type, code: options?.code, statusCode: options?.statusCode, details: options?.details, recoverable: options?.recoverable ?? false, }); return error; } /** * Extract error message from unknown error */ export function extractErrorMessage(error) { if (error == null) { return 'Unknown error'; } if (typeof error === 'string') { return error; } if (error instanceof Error) { return error.message; } if (typeof error === 'object') { const e = error; if (typeof e['message'] === 'string') { return e['message']; } if (typeof e['error'] === 'string') { return e['error']; } if (typeof e['reason'] === 'string') { return e['reason']; } if (typeof e['code'] === 'string') { return e['code']; } } try { return JSON.stringify(error); } catch { return '[object]'; } } /** * Extract error code from unknown error */ export function extractErrorCode(error) { if (error == null) { return undefined; } if (typeof error === 'object') { const e = error; if (typeof e['code'] === 'string') { return e['code']; } if (typeof e['errno'] === 'string') { return e['errno']; } if (typeof e['syscall'] === 'string') { return e['syscall']; } } return undefined; } /** * Classify error type from error object */ export function classifyErrorType(error) { const message = extractErrorMessage(error).toLowerCase(); const code = extractErrorCode(error)?.toLowerCase(); // Check for typed errors first if (error != null && typeof error === 'object' && 'type' in error) { const typedError = error; if (typeof typedError.type === 'string') { return typedError.type; } } // Network errors if (code === 'econnrefused' || code === 'econnreset' || code === 'enotfound' || code === 'enetunreach' || code === 'ehostunreach' || message.includes('connect') || message.includes('network')) { return ErrorType.Network; } // Timeout errors if (code === 'etimedout' || code === 'timeout' || message.includes('timeout') || message.includes('timed out')) { return ErrorType.Timeout; } // Authentication errors if (code === 'eauth' || message.includes('auth') || message.includes('password') || message.includes('credential') || message.includes('permission denied')) { return ErrorType.Authentication; } // Permission errors if (code === 'eacces' || code === 'eperm' || message.includes('permission') || message.includes('access denied')) { return ErrorType.Permission; } // Validation errors if (message.includes('invalid') || message.includes('required') || message.includes('must be') || message.includes('validation')) { return ErrorType.Validation; } // Not found errors if (code === 'enoent' || message.includes('not found') || message.includes('does not exist')) { return ErrorType.NotFound; } // Configuration errors if (message.includes('config') || message.includes('setting') || message.includes('option')) { return ErrorType.Configuration; } return ErrorType.Unknown; } /** * Convert error type to HTTP status code */ export function errorTypeToStatusCode(type) { switch (type) { case ErrorType.Validation: return 400; // Bad Request case ErrorType.Authentication: return 401; // Unauthorized case ErrorType.Permission: return 403; // Forbidden case ErrorType.NotFound: return 404; // Not Found case ErrorType.Conflict: return 409; // Conflict case ErrorType.RateLimit: return 429; // Too Many Requests case ErrorType.Network: return 502; // Bad Gateway case ErrorType.Timeout: return 504; // Gateway Timeout case ErrorType.Configuration: case ErrorType.Internal: case ErrorType.Unknown: default: return 500; // Internal Server Error } } /** * Convert SSH error type to standard error type */ export function sshErrorTypeToErrorType(sshType) { switch (sshType) { case 'auth': return ErrorType.Authentication; case 'network': return ErrorType.Network; case 'timeout': return ErrorType.Timeout; case 'permission': return ErrorType.Permission; case 'protocol': return ErrorType.Validation; default: return ErrorType.Unknown; } } /** * Wrap function execution in try-catch and return Result */ export function tryExecute(fn, errorType) { try { return ok(fn()); } catch (error) { const message = extractErrorMessage(error); const type = errorType ?? classifyErrorType(error); const code = extractErrorCode(error); return err(createTypedError(message, type, { ...(code != null && { code }), details: error, })); } } /** * Wrap async function execution in try-catch and return Result */ export async function tryExecuteAsync(fn, errorType) { try { const value = await fn(); return ok(value); } catch (error) { const message = extractErrorMessage(error); const type = errorType ?? classifyErrorType(error); const code = extractErrorCode(error); return err(createTypedError(message, type, { ...(code != null && { code }), details: error, })); } } /** * Log error with context */ export function logError(context, error, details) { const message = extractErrorMessage(error); const code = extractErrorCode(error); const type = classifyErrorType(error); debug('%s: %s (type: %s, code: %s)', context, message, type, code ?? 'none'); if (details != null) { debug('%s: details: %O', context, details); } // Log stack trace for internal errors if (type === ErrorType.Internal && error instanceof Error && error.stack != null) { debug('%s: stack: %s', context, error.stack); } } /** * Format error for API response */ export function formatErrorResponse(error, includeDetails = false) { const message = extractErrorMessage(error); const type = classifyErrorType(error); const code = extractErrorCode(error); const statusCode = errorTypeToStatusCode(type); return { error: { message, type, ...(code != null && { code }), statusCode, timestamp: new Date(), ...(includeDetails && { details: error }), } }; } /** * Default recovery strategy */ export const defaultRecoveryStrategy = { maxAttempts: 3, delay: 1000, backoff: 2, shouldRetry: (error, attempt) => { const type = classifyErrorType(error); // Retry network and timeout errors return attempt < 3 && (type === ErrorType.Network || type === ErrorType.Timeout); } }; /** * Execute with retry on failure */ export async function executeWithRetry(fn, strategy = defaultRecoveryStrategy) { let lastError; for (let attempt = 1; attempt <= strategy.maxAttempts; attempt++) { try { const result = await fn(); return ok(result); } catch (error) { lastError = error; logError(`Attempt ${attempt} failed`, error); if (!strategy.shouldRetry(error, attempt)) { break; } if (attempt < strategy.maxAttempts) { const delay = strategy.delay * Math.pow(strategy.backoff ?? 1, attempt - 1); await new Promise(resolve => setTimeout(resolve, delay)); } } } const message = extractErrorMessage(lastError); const type = classifyErrorType(lastError); const code = extractErrorCode(lastError); return err(createTypedError(message, type, { ...(code != null && { code }), details: lastError, })); }