error-handler.ts•10.6 kB
/**
* Comprehensive error handling utilities
*/
import {
GoogleDriveMCPError,
ErrorCategory,
ErrorResponse,
ErrorHandlerConfig,
DEFAULT_ERROR_CONFIG,
AuthenticationError,
TokenExpiredError,
PermissionDeniedError,
InsufficientScopeError,
GoogleAPIError,
NetworkError,
TimeoutError,
FileProcessingError,
ValidationError,
InvalidFileIdError,
RateLimitError,
QuotaExceededError,
InternalError
} from '../types/errors.js';
import { Logger } from '../logging/logger.js';
/**
* Central error handler for the Google Drive MCP Server
*/
export class ErrorHandler {
private config: ErrorHandlerConfig;
private logger: Logger;
constructor(config: Partial<ErrorHandlerConfig> = {}, logger?: Logger) {
this.config = { ...DEFAULT_ERROR_CONFIG, ...config };
this.logger = logger || new Logger();
}
/**
* Handle and classify any error
*/
handleError(error: unknown, context?: Record<string, any>): GoogleDriveMCPError {
// If it's already a GoogleDriveMCPError, just log and return
if (error instanceof GoogleDriveMCPError) {
this.logError(error, context);
return error;
}
// Classify and convert other error types
const classifiedError = this.classifyError(error, context);
this.logError(classifiedError, context);
return classifiedError;
}
/**
* Classify unknown errors into appropriate GoogleDriveMCPError types
*/
private classifyError(error: unknown, context?: Record<string, any>): GoogleDriveMCPError {
const errorMessage = this.extractErrorMessage(error);
const errorContext = { ...context, originalError: this.serializeError(error) };
// Handle Google API errors
if (this.isGoogleAPIError(error)) {
return this.handleGoogleAPIError(error, errorContext);
}
// Handle network errors
if (this.isNetworkError(error)) {
return new NetworkError(errorMessage, errorContext);
}
// Handle timeout errors
if (this.isTimeoutError(error)) {
return new TimeoutError(errorMessage, errorContext);
}
// Handle authentication errors
if (this.isAuthenticationError(error)) {
return new AuthenticationError(errorMessage, errorContext);
}
// Handle file processing errors
if (this.isFileProcessingError(error)) {
return new FileProcessingError(errorMessage, errorContext);
}
// Handle validation errors
if (this.isValidationError(error)) {
return new ValidationError(errorMessage, errorContext);
}
// Default to internal error
return new InternalError(
`Unhandled error: ${errorMessage}`,
errorContext
);
}
/**
* Handle Google API specific errors
*/
private handleGoogleAPIError(error: any, context: Record<string, any>): GoogleDriveMCPError {
const statusCode = this.extractStatusCode(error);
const errorMessage = this.extractErrorMessage(error);
switch (statusCode) {
case 401:
if (errorMessage.toLowerCase().includes('token')) {
return new TokenExpiredError(errorMessage, context);
}
return new AuthenticationError(errorMessage, context);
case 403:
if (errorMessage.toLowerCase().includes('insufficient')) {
return new InsufficientScopeError(errorMessage, context);
}
if (errorMessage.toLowerCase().includes('quota')) {
return new QuotaExceededError(errorMessage, context);
}
return new PermissionDeniedError(errorMessage, context);
case 404:
return new InvalidFileIdError(
`File not found: ${errorMessage}`,
context
);
case 429:
return new RateLimitError(errorMessage, context);
default:
return new GoogleAPIError(errorMessage, statusCode, context);
}
}
/**
* Log error with appropriate level and details
*/
private logError(error: GoogleDriveMCPError, context?: Record<string, any>): void {
if (!this.config.logErrors) return;
const logData = {
code: error.code,
category: error.category,
message: error.message,
retryable: error.retryable,
timestamp: error.timestamp,
context: { ...error.context, ...context }
};
if (this.config.includeStackTrace && error.stack) {
logData.context = { ...logData.context, stack: error.stack };
}
// Log with appropriate level based on error category
switch (error.category) {
case ErrorCategory.INTERNAL:
this.logger.error('Internal error occurred', error, logData.context);
break;
case ErrorCategory.AUTHENTICATION:
case ErrorCategory.AUTHORIZATION:
this.logger.warn('Authentication/Authorization error', logData);
break;
case ErrorCategory.CONFIGURATION:
this.logger.error('Configuration error', error, logData.context);
break;
case ErrorCategory.NETWORK:
case ErrorCategory.API_ERROR:
if (error.retryable) {
this.logger.warn('Retryable error occurred', logData);
} else {
this.logger.error('Non-retryable error occurred', error, logData.context);
}
break;
default:
this.logger.info('Error handled', logData);
}
}
/**
* Create graceful degradation response
*/
createGracefulResponse(error: GoogleDriveMCPError, fallbackData?: any): {
success: boolean;
data?: any;
error: ErrorResponse;
degraded: boolean;
} {
return {
success: false,
data: fallbackData,
error: error.toJSON(),
degraded: !!fallbackData
};
}
/**
* Check if error should trigger circuit breaker
*/
shouldTriggerCircuitBreaker(error: GoogleDriveMCPError): boolean {
if (!this.config.enableCircuitBreaker) return false;
// Trigger circuit breaker for certain error categories
const triggerCategories = [
ErrorCategory.API_ERROR,
ErrorCategory.NETWORK,
ErrorCategory.RATE_LIMIT,
ErrorCategory.QUOTA
];
return triggerCategories.includes(error.category) && !error.retryable;
}
/**
* Extract error message from unknown error
*/
private extractErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object' && 'message' in error) {
return String((error as any).message);
}
return 'Unknown error occurred';
}
/**
* Extract status code from error
*/
private extractStatusCode(error: any): number | undefined {
// Google API client error format
if (error?.response?.status) {
return error.response.status;
}
if (error?.status) {
return error.status;
}
if (error?.code && typeof error.code === 'number') {
return error.code;
}
return undefined;
}
/**
* Serialize error for logging
*/
private serializeError(error: unknown): any {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: this.config.includeStackTrace ? error.stack : undefined
};
}
return error;
}
/**
* Check if error is from Google API
*/
private isGoogleAPIError(error: any): boolean {
return (
error?.response?.status ||
error?.status ||
(error?.message && error.message.includes('googleapis')) ||
(error?.code && typeof error.code === 'number')
);
}
/**
* Check if error is network-related
*/
private isNetworkError(error: any): boolean {
const networkKeywords = ['network', 'connection', 'econnrefused', 'enotfound', 'etimedout'];
const errorMessage = this.extractErrorMessage(error).toLowerCase();
return networkKeywords.some(keyword => errorMessage.includes(keyword)) ||
error?.code === 'ECONNREFUSED' ||
error?.code === 'ENOTFOUND' ||
error?.code === 'ETIMEDOUT';
}
/**
* Check if error is timeout-related
*/
private isTimeoutError(error: any): boolean {
const timeoutKeywords = ['timeout', 'timed out'];
const errorMessage = this.extractErrorMessage(error).toLowerCase();
return timeoutKeywords.some(keyword => errorMessage.includes(keyword)) ||
error?.code === 'ETIMEDOUT' ||
error?.code === 'TIMEOUT';
}
/**
* Check if error is authentication-related
*/
private isAuthenticationError(error: any): boolean {
const authKeywords = ['unauthorized', 'authentication', 'invalid_grant', 'invalid_client'];
const errorMessage = this.extractErrorMessage(error).toLowerCase();
return authKeywords.some(keyword => errorMessage.includes(keyword));
}
/**
* Check if error is file processing-related
*/
private isFileProcessingError(error: any): boolean {
const processingKeywords = ['parse', 'processing', 'corrupt', 'invalid format', 'unsupported'];
const errorMessage = this.extractErrorMessage(error).toLowerCase();
return processingKeywords.some(keyword => errorMessage.includes(keyword));
}
/**
* Check if error is validation-related
*/
private isValidationError(error: any): boolean {
const validationKeywords = ['validation', 'invalid', 'required', 'missing'];
const errorMessage = this.extractErrorMessage(error).toLowerCase();
return validationKeywords.some(keyword => errorMessage.includes(keyword));
}
}
/**
* Global error handler instance
*/
export const globalErrorHandler = new ErrorHandler();
/**
* Utility function to handle errors in async operations
*/
export async function handleAsyncError<T>(
operation: () => Promise<T>,
context?: Record<string, any>,
errorHandler: ErrorHandler = globalErrorHandler
): Promise<{ success: true; data: T } | { success: false; error: GoogleDriveMCPError }> {
try {
const data = await operation();
return { success: true, data };
} catch (error) {
const handledError = errorHandler.handleError(error, context);
return { success: false, error: handledError };
}
}
/**
* Utility function to wrap sync operations with error handling
*/
export function handleSyncError<T>(
operation: () => T,
context?: Record<string, any>,
errorHandler: ErrorHandler = globalErrorHandler
): { success: true; data: T } | { success: false; error: GoogleDriveMCPError } {
try {
const data = operation();
return { success: true, data };
} catch (error) {
const handledError = errorHandler.handleError(error, context);
return { success: false, error: handledError };
}
}