Skip to main content
Glama
errors.test.ts26.3 kB
import { ErrorHandler, MCPErrorCodes } from '../../src/utils/errors.js'; import { AppError, ErrorType } from '../../src/types/index.js'; describe('ErrorHandler', () => { describe('translateToMCPError', () => { it('should translate AUTH_ERROR to MCP authentication error', () => { const error = new AppError( ErrorType.AUTH_ERROR, 'Invalid token', { responseData: { error: 'Unauthorized' } }, 401 ); const mcpError = ErrorHandler.translateToMCPError(error, 'test-context'); expect(mcpError.code).toBe(MCPErrorCodes.AUTHENTICATION_ERROR); expect(mcpError.message).toBe('Authentication failed. Please check your API token and ensure it is valid and not expired.'); expect(mcpError.data).toMatchObject({ context: 'test-context', errorType: ErrorType.AUTH_ERROR, originalMessage: 'Invalid token', details: { responseData: { error: 'Unauthorized' } }, status: 401, authenticationRequired: true }); expect(mcpError.data.timestamp).toBeDefined(); }); it('should translate 403 AUTH_ERROR to access forbidden', () => { const error = new AppError( ErrorType.AUTH_ERROR, 'Access denied', {}, 403 ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.message).toBe('Access forbidden. Your API token does not have sufficient permissions for this operation.'); expect(mcpError.code).toBe(MCPErrorCodes.AUTHENTICATION_ERROR); }); it('should translate VALIDATION_ERROR to MCP validation error', () => { const error = new AppError( ErrorType.VALIDATION_ERROR, 'Invalid parameters', { validationErrors: [{ field: 'name', message: 'required' }] }, 422 ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.VALIDATION_ERROR); expect(mcpError.message).toBe('Parameter validation failed: Invalid parameters'); expect(mcpError.data).toMatchObject({ errorType: ErrorType.VALIDATION_ERROR, originalMessage: 'Invalid parameters', details: { validationErrors: [{ field: 'name', message: 'required' }] }, status: 422, validationErrors: [{ field: 'name', message: 'required' }] }); expect(mcpError.data.timestamp).toBeDefined(); }); it('should translate timeout NETWORK_ERROR to timeout error', () => { const error = new AppError( ErrorType.NETWORK_ERROR, 'Request timeout', { code: 'ECONNABORTED' } ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.TIMEOUT_ERROR); expect(mcpError.message).toBe('Request timed out. The API did not respond within the expected time. Please try again.'); expect(mcpError.data.retryable).toBe(true); expect(mcpError.data.networkIssue).toBe(true); }); it('should translate connection reset NETWORK_ERROR', () => { const error = new AppError( ErrorType.NETWORK_ERROR, 'Connection reset', { code: 'ECONNRESET' } ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.NETWORK_ERROR); expect(mcpError.message).toBe('Connection was reset. Please check your network connection and try again.'); }); it('should translate rate limit API_ERROR', () => { const error = new AppError( ErrorType.API_ERROR, 'Too many requests', { retryAfter: '60' }, 429 ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.RATE_LIMIT_ERROR); expect(mcpError.message).toBe('Rate limit exceeded: Too many requests'); expect(mcpError.data.retryable).toBe(true); expect(mcpError.data.retryAfter).toBe('60'); }); it('should translate server API_ERROR', () => { const error = new AppError( ErrorType.API_ERROR, 'Internal server error', {}, 500 ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.API_ERROR); expect(mcpError.message).toBe('API request failed: Internal server error'); expect(mcpError.data.retryable).toBe(true); }); it('should translate client API_ERROR as non-retryable', () => { const error = new AppError( ErrorType.API_ERROR, 'Bad request', {}, 400 ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.API_ERROR); expect(mcpError.data.retryable).toBe(false); }); it('should translate TOOL_ERROR', () => { const error = new AppError( ErrorType.TOOL_ERROR, 'Tool execution failed', { toolName: 'test-tool', toolParams: { id: 1 } } ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.TOOL_ERROR); expect(mcpError.message).toBe('Tool execution failed: Tool execution failed'); expect(mcpError.data.toolName).toBe('test-tool'); expect(mcpError.data.toolParams).toEqual({ id: 1 }); }); it('should translate CONFIG_ERROR', () => { const error = new AppError( ErrorType.CONFIG_ERROR, 'Missing API token' ); const mcpError = ErrorHandler.translateToMCPError(error); expect(mcpError.code).toBe(MCPErrorCodes.INVALID_PARAMS); expect(mcpError.message).toBe('Configuration error: Missing API token'); expect(mcpError.data.configurationRequired).toBe(true); }); it('should translate generic Error to internal error', () => { const error = new Error('Something went wrong'); const mcpError = ErrorHandler.translateToMCPError(error, 'test-context'); expect(mcpError.code).toBe(MCPErrorCodes.INTERNAL_ERROR); expect(mcpError.message).toBe('Internal error: Something went wrong'); expect(mcpError.data.context).toBe('test-context'); expect(mcpError.data.originalError).toBe('Something went wrong'); }); }); describe('createToolErrorResponse', () => { it('should create tool error response from AppError', () => { const error = new AppError( ErrorType.API_ERROR, 'API call failed', { status: 500 } ); const mcpError = ErrorHandler.createToolErrorResponse( error, 'test-tool', { param1: 'value1' }, 'tool-execution' ); expect(mcpError.code).toBe(MCPErrorCodes.TOOL_ERROR); expect(mcpError.message).toBe('Tool execution failed: API call failed'); expect(mcpError.data.toolName).toBe('test-tool'); expect(mcpError.data.toolParams).toEqual({ param1: 'value1' }); expect(mcpError.data.context).toBe('tool-execution'); expect(mcpError.data.details.originalType).toBe(ErrorType.API_ERROR); }); it('should create tool error response from generic Error', () => { const error = new Error('Generic error'); const mcpError = ErrorHandler.createToolErrorResponse( error, 'test-tool', { param1: 'value1' } ); expect(mcpError.code).toBe(MCPErrorCodes.TOOL_ERROR); expect(mcpError.message).toBe('Tool execution failed: Generic error'); expect(mcpError.data.toolName).toBe('test-tool'); expect(mcpError.data.toolParams).toEqual({ param1: 'value1' }); }); }); describe('getUserFriendlyMessage', () => { it('should return friendly message for AUTH_ERROR', () => { const error = new AppError(ErrorType.AUTH_ERROR, 'Invalid token', {}, 401); const message = ErrorHandler.getUserFriendlyMessage(error); expect(message).toBe('Authentication failed. Please check your API token and ensure it is valid and not expired.'); }); it('should return friendly message for NETWORK_ERROR timeout', () => { const error = new AppError( ErrorType.NETWORK_ERROR, 'timeout', { code: 'ECONNABORTED' } ); const message = ErrorHandler.getUserFriendlyMessage(error); expect(message).toBe('Request timed out. The API did not respond within the expected time. Please try again.'); }); it('should return friendly message for rate limit', () => { const error = new AppError(ErrorType.API_ERROR, 'Rate limited', {}, 429); const message = ErrorHandler.getUserFriendlyMessage(error); expect(message).toBe('Rate limit exceeded. Please wait before making more requests.'); }); it('should return friendly message for generic Error', () => { const error = new Error('Something broke'); const message = ErrorHandler.getUserFriendlyMessage(error); expect(message).toBe('An unexpected error occurred: Something broke'); }); }); describe('isRetryableError', () => { it('should identify retryable network errors', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Connection failed'); expect(ErrorHandler.isRetryableError(error)).toBe(true); }); it('should identify retryable server errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Server error', {}, 500); expect(ErrorHandler.isRetryableError(error)).toBe(true); }); it('should identify retryable rate limit errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Rate limited', {}, 429); expect(ErrorHandler.isRetryableError(error)).toBe(true); }); it('should identify non-retryable client errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Bad request', {}, 400); expect(ErrorHandler.isRetryableError(error)).toBe(false); }); it('should identify non-retryable auth errors', () => { const error = new AppError(ErrorType.AUTH_ERROR, 'Unauthorized', {}, 401); expect(ErrorHandler.isRetryableError(error)).toBe(false); }); it('should identify non-retryable generic errors', () => { const error = new Error('Generic error'); expect(ErrorHandler.isRetryableError(error)).toBe(false); }); }); describe('getRetryDelay', () => { it('should return retry-after delay for rate limit errors', () => { const error = new AppError( ErrorType.API_ERROR, 'Rate limited', { retryAfter: '30' }, 429 ); const delay = ErrorHandler.getRetryDelay(error, 1); expect(delay).toBe(30000); // 30 seconds in milliseconds }); it('should return exponential backoff for network errors', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Connection failed'); expect(ErrorHandler.getRetryDelay(error, 1)).toBe(1000); expect(ErrorHandler.getRetryDelay(error, 2)).toBe(2000); expect(ErrorHandler.getRetryDelay(error, 3)).toBe(4000); expect(ErrorHandler.getRetryDelay(error, 10)).toBe(30000); // Capped at 30 seconds }); it('should return default backoff for other errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Server error', {}, 500); expect(ErrorHandler.getRetryDelay(error, 1)).toBe(1000); expect(ErrorHandler.getRetryDelay(error, 2)).toBe(2000); expect(ErrorHandler.getRetryDelay(error, 3)).toBe(3000); }); it('should return default backoff for generic errors', () => { const error = new Error('Generic error'); expect(ErrorHandler.getRetryDelay(error, 1)).toBe(1000); expect(ErrorHandler.getRetryDelay(error, 2)).toBe(2000); }); }); describe('logError', () => { let consoleSpy: jest.SpyInstance; beforeEach(() => { consoleSpy = jest.spyOn(console, 'error').mockImplementation(); }); afterEach(() => { consoleSpy.mockRestore(); }); it('should log AppError details', () => { const error = new AppError( ErrorType.API_ERROR, 'API failed', { status: 500 }, 500 ); ErrorHandler.logError(error, 'test-context'); const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]); expect(loggedData.message).toBe('API failed'); expect(loggedData.type).toBe('API_ERROR'); expect(loggedData.context).toBe('test-context'); expect(loggedData.status).toBe(500); }); it('should log generic Error', () => { const error = new Error('Generic error'); ErrorHandler.logError(error); const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]); expect(loggedData.message).toBe('Generic error'); expect(loggedData.type).toBe('UnknownError'); }); it('should use custom logger when provided', () => { const mockLogger = { error: jest.fn() }; const error = new AppError(ErrorType.API_ERROR, 'Test error'); ErrorHandler.logError(error, 'test-context', mockLogger); expect(mockLogger.error).toHaveBeenCalledWith( 'Error occurred:', expect.objectContaining({ message: 'Test error', type: ErrorType.API_ERROR, context: 'test-context' }) ); expect(consoleSpy).not.toHaveBeenCalled(); }); }); describe('validateMCPErrorResponse', () => { it('should validate valid MCP error response', () => { const validError = { code: MCPErrorCodes.API_ERROR, message: 'Test error' }; expect(ErrorHandler.validateMCPErrorResponse(validError)).toBe(true); }); it('should reject invalid error response - missing code', () => { const invalidError = { message: 'Test error' }; expect(ErrorHandler.validateMCPErrorResponse(invalidError)).toBe(false); }); it('should reject invalid error response - missing message', () => { const invalidError = { code: MCPErrorCodes.API_ERROR }; expect(ErrorHandler.validateMCPErrorResponse(invalidError)).toBe(false); }); it('should reject invalid error response - invalid code', () => { const invalidError = { code: 999999, message: 'Test error' }; expect(ErrorHandler.validateMCPErrorResponse(invalidError)).toBe(false); }); it('should reject non-object error response', () => { expect(ErrorHandler.validateMCPErrorResponse(null)).toBe(false); expect(ErrorHandler.validateMCPErrorResponse('string')).toBe(false); expect(ErrorHandler.validateMCPErrorResponse(123)).toBe(false); }); }); describe('createProtocolError', () => { it('should create protocol error with message', () => { const error = ErrorHandler.createProtocolError('Invalid request format'); expect(error.code).toBe(MCPErrorCodes.INVALID_REQUEST); expect(error.message).toBe('Protocol error: Invalid request format'); expect(error.data.protocolViolation).toBe(true); expect(error.data.timestamp).toBeDefined(); }); it('should create protocol error with additional data', () => { const error = ErrorHandler.createProtocolError('Invalid request', { requestId: '123' }); expect(error.code).toBe(MCPErrorCodes.INVALID_REQUEST); expect(error.data.requestId).toBe('123'); expect(error.data.protocolViolation).toBe(true); }); }); describe('createMethodNotFoundError', () => { it('should create method not found error', () => { const error = ErrorHandler.createMethodNotFoundError('unknown/method'); expect(error.code).toBe(MCPErrorCodes.METHOD_NOT_FOUND); expect(error.message).toBe('Method not found: unknown/method'); expect(error.data.method).toBe('unknown/method'); expect(error.data.availableMethods).toEqual(['tools/list', 'tools/call']); }); }); describe('createParseError', () => { it('should create parse error from original error', () => { const originalError = new Error('Unexpected token'); const error = ErrorHandler.createParseError(originalError); expect(error.code).toBe(MCPErrorCodes.PARSE_ERROR); expect(error.message).toBe('Parse error: Invalid JSON was received by the server'); expect(error.data.originalError).toBe('Unexpected token'); expect(error.data.parseError).toBe(true); }); }); describe('withErrorHandling', () => { it('should execute operation successfully', async () => { const operation = jest.fn().mockResolvedValue('success'); const context = { operation: 'test-op' }; const result = await ErrorHandler.withErrorHandling(operation, context); expect(result).toBe('success'); expect(operation).toHaveBeenCalled(); }); it('should handle operation failure', async () => { const operation = jest.fn().mockRejectedValue(new Error('Operation failed')); const context = { operation: 'test-op' }; await expect(ErrorHandler.withErrorHandling(operation, context)).rejects.toThrow('Operation failed'); }); }); describe('getErrorSeverity', () => { it('should return critical for config errors', () => { const error = new AppError(ErrorType.CONFIG_ERROR, 'Missing config'); expect(ErrorHandler.getErrorSeverity(error)).toBe('critical'); }); it('should return error for auth errors', () => { const error = new AppError(ErrorType.AUTH_ERROR, 'Unauthorized', {}, 401); expect(ErrorHandler.getErrorSeverity(error)).toBe('error'); }); it('should return warning for forbidden errors', () => { const error = new AppError(ErrorType.AUTH_ERROR, 'Forbidden', {}, 403); expect(ErrorHandler.getErrorSeverity(error)).toBe('warning'); }); it('should return error for server API errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Server error', {}, 500); expect(ErrorHandler.getErrorSeverity(error)).toBe('error'); }); it('should return warning for rate limit errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Rate limited', {}, 429); expect(ErrorHandler.getErrorSeverity(error)).toBe('warning'); }); it('should return info for client errors', () => { const error = new AppError(ErrorType.API_ERROR, 'Bad request', {}, 400); expect(ErrorHandler.getErrorSeverity(error)).toBe('info'); }); it('should return warning for network errors', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Connection failed'); expect(ErrorHandler.getErrorSeverity(error)).toBe('warning'); }); it('should return info for validation errors', () => { const error = new AppError(ErrorType.VALIDATION_ERROR, 'Invalid input'); expect(ErrorHandler.getErrorSeverity(error)).toBe('info'); }); it('should return error for tool errors', () => { const error = new AppError(ErrorType.TOOL_ERROR, 'Tool failed'); expect(ErrorHandler.getErrorSeverity(error)).toBe('error'); }); it('should return error for generic errors', () => { const error = new Error('Generic error'); expect(ErrorHandler.getErrorSeverity(error)).toBe('error'); }); }); describe('isSystemHealthIssue', () => { it('should identify config errors as health issues', () => { const error = new AppError(ErrorType.CONFIG_ERROR, 'Missing config'); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(true); }); it('should identify server API errors as health issues', () => { const error = new AppError(ErrorType.API_ERROR, 'Server error', {}, 500); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(true); }); it('should identify network errors as health issues', () => { const error = new AppError(ErrorType.NETWORK_ERROR, 'Connection failed'); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(true); }); it('should not identify client errors as health issues', () => { const error = new AppError(ErrorType.API_ERROR, 'Bad request', {}, 400); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(false); }); it('should not identify auth errors as health issues', () => { const error = new AppError(ErrorType.AUTH_ERROR, 'Unauthorized'); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(false); }); it('should identify generic errors as health issues', () => { const error = new Error('Generic error'); expect(ErrorHandler.isSystemHealthIssue(error)).toBe(true); }); }); describe('createErrorMetrics', () => { it('should create metrics for AppError', () => { const error = new AppError(ErrorType.API_ERROR, 'API failed', {}, 500); const context = { operation: 'test-op', toolName: 'test-tool' }; const metrics = ErrorHandler.createErrorMetrics(error, context); expect(metrics).toMatchObject({ errorType: ErrorType.API_ERROR, severity: 'error', isHealthIssue: true, isRetryable: true, message: 'API failed', httpStatus: 500, operation: 'test-op', toolName: 'test-tool' }); expect(metrics.timestamp).toBeDefined(); }); it('should create metrics for generic Error', () => { const error = new Error('Generic error'); const metrics = ErrorHandler.createErrorMetrics(error); expect(metrics).toMatchObject({ errorType: 'UnknownError', severity: 'error', isHealthIssue: true, isRetryable: false, message: 'Generic error' }); expect(metrics.timestamp).toBeDefined(); }); it('should handle errors with circular references in details', () => { const circularObj: any = { name: 'test' }; circularObj.self = circularObj; const error = new AppError(ErrorType.API_ERROR, 'Circular error', circularObj); const metrics = ErrorHandler.createErrorMetrics(error); expect(metrics.errorType).toBe(ErrorType.API_ERROR); expect(metrics.message).toBe('Circular error'); }); it('should handle errors with very long messages', () => { const longMessage = 'Error message '.repeat(1000); const error = new AppError(ErrorType.API_ERROR, longMessage); const metrics = ErrorHandler.createErrorMetrics(error); expect(metrics.message).toBe(longMessage); }); it('should handle errors with special characters in messages', () => { const specialMessage = 'Error with special chars: 测试 🔥 émoji & symbols!@#$%^&*()'; const error = new AppError(ErrorType.API_ERROR, specialMessage); const metrics = ErrorHandler.createErrorMetrics(error); expect(metrics.message).toBe(specialMessage); }); }); describe('Performance and Memory Tests', () => { it('should handle large number of error translations efficiently', () => { const startTime = Date.now(); for (let i = 0; i < 1000; i++) { const error = new AppError(ErrorType.API_ERROR, `Error ${i}`, {}, 500); ErrorHandler.translateToMCPError(error); } const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(1000); // Should complete in under 1 second }); it('should handle errors with large detail objects', () => { const largeDetails = { data: 'x'.repeat(100000), // 100KB string array: new Array(10000).fill('item'), nested: { level1: { level2: { level3: 'deep nesting' } } } }; const error = new AppError(ErrorType.API_ERROR, 'Large error', largeDetails); expect(() => ErrorHandler.translateToMCPError(error)).not.toThrow(); expect(() => ErrorHandler.createErrorMetrics(error)).not.toThrow(); }); it('should handle concurrent error processing', async () => { const errors = Array.from({ length: 100 }, (_, i) => new AppError(ErrorType.API_ERROR, `Concurrent error ${i}`, {}, 500) ); const promises = errors.map(error => Promise.resolve(ErrorHandler.translateToMCPError(error)) ); const results = await Promise.all(promises); expect(results).toHaveLength(100); results.forEach((result, i) => { expect(result.message).toContain(`Concurrent error ${i}`); }); }); }); describe('Error Context Handling', () => { it('should handle undefined context gracefully', () => { const error = new AppError(ErrorType.API_ERROR, 'Test error'); const mcpError = ErrorHandler.translateToMCPError(error, undefined); expect(mcpError.data.context).toBeUndefined(); }); it('should handle null context gracefully', () => { const error = new AppError(ErrorType.API_ERROR, 'Test error'); const mcpError = ErrorHandler.translateToMCPError(error, null); // When context is null, it gets spread into baseData, so individual properties are undefined expect(mcpError.data).toBeDefined(); expect(mcpError.data.errorType).toBe(ErrorType.API_ERROR); }); it('should handle complex context objects', () => { const complexContext = { operation: 'complex-op', metadata: { userId: 'user123', sessionId: 'session456', requestId: 'req789' }, timing: { startTime: Date.now(), duration: 1500 } }; const error = new AppError(ErrorType.API_ERROR, 'Test error'); const mcpError = ErrorHandler.translateToMCPError(error, complexContext); // The complex context gets spread into the data object expect(mcpError.data.operation).toBe('complex-op'); expect(mcpError.data.metadata).toEqual(complexContext.metadata); expect(mcpError.data.timing).toEqual(complexContext.timing); }); it('should handle context with circular references', () => { const circularContext: any = { operation: 'test' }; circularContext.self = circularContext; const error = new AppError(ErrorType.API_ERROR, 'Test error'); expect(() => ErrorHandler.translateToMCPError(error, circularContext)).not.toThrow(); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/celeryhq/simplified-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server