UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

207 lines (206 loc) • 9.22 kB
"use strict"; /** * @moduleName: Error Handler - Centralized Error Management * @version: 1.0.0 * @since: 2025-07-25 * @projectSummary: Centralized error handling system with standardized responses and security * @techStack: TypeScript, Express.js, Logging * @dependency: express * @requirementsTraceability: * {@link Requirements.REQ_ERROR_001} (Centralized Error Handling) * {@link Requirements.REQ_ERROR_002} (Validation Error Handling) * {@link Requirements.REQ_ERROR_003} (Database Error Recovery) * @briefDescription: Provides consistent error handling, logging, and secure error responses */ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateQuizFormat = exports.validateRequired = exports.asyncHandler = exports.errorHandler = exports.createInternalError = exports.createDatabaseError = exports.createNotFoundError = exports.createValidationError = exports.Logger = exports.AppError = exports.ErrorCode = void 0; const uuid_1 = require("uuid"); // Error types var ErrorCode; (function (ErrorCode) { // Client errors (4xx) ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR"; ErrorCode["QUIZ_NOT_FOUND"] = "QUIZ_NOT_FOUND"; ErrorCode["INVALID_QUIZ_FORMAT"] = "INVALID_QUIZ_FORMAT"; ErrorCode["MISSING_REQUIRED_FIELDS"] = "MISSING_REQUIRED_FIELDS"; ErrorCode["INVALID_REQUEST_FORMAT"] = "INVALID_REQUEST_FORMAT"; // Server errors (5xx) ErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR"; ErrorCode["FILE_SYSTEM_ERROR"] = "FILE_SYSTEM_ERROR"; ErrorCode["INTERNAL_SERVER_ERROR"] = "INTERNAL_SERVER_ERROR"; ErrorCode["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE"; ErrorCode["QUIZ_CREATION_ERROR"] = "QUIZ_CREATION_ERROR"; ErrorCode["QUIZ_RETRIEVAL_ERROR"] = "QUIZ_RETRIEVAL_ERROR"; })(ErrorCode || (exports.ErrorCode = ErrorCode = {})); // Custom error class class AppError extends Error { constructor(code, message, httpStatus = 500, details) { super(message); this.name = 'AppError'; this.code = code; this.httpStatus = httpStatus; this.correlationId = (0, uuid_1.v4)(); this.timestamp = new Date().toISOString(); this.details = details; // Maintain proper stack trace Error.captureStackTrace(this, AppError); } toJSON() { return { code: this.code, message: this.message, details: this.details, correlationId: this.correlationId, timestamp: this.timestamp, httpStatus: this.httpStatus, }; } } exports.AppError = AppError; // HTTP status code mapping const ERROR_STATUS_MAP = { [ErrorCode.VALIDATION_ERROR]: 400, [ErrorCode.QUIZ_NOT_FOUND]: 404, [ErrorCode.INVALID_QUIZ_FORMAT]: 400, [ErrorCode.MISSING_REQUIRED_FIELDS]: 400, [ErrorCode.INVALID_REQUEST_FORMAT]: 400, [ErrorCode.DATABASE_ERROR]: 500, [ErrorCode.FILE_SYSTEM_ERROR]: 500, [ErrorCode.INTERNAL_SERVER_ERROR]: 500, [ErrorCode.SERVICE_UNAVAILABLE]: 503, [ErrorCode.QUIZ_CREATION_ERROR]: 500, [ErrorCode.QUIZ_RETRIEVAL_ERROR]: 500, }; // Security-safe error messages for production const SAFE_ERROR_MESSAGES = { [ErrorCode.VALIDATION_ERROR]: 'Invalid input provided', [ErrorCode.QUIZ_NOT_FOUND]: 'The requested quiz was not found', [ErrorCode.INVALID_QUIZ_FORMAT]: 'Quiz format is invalid', [ErrorCode.MISSING_REQUIRED_FIELDS]: 'Required fields are missing', [ErrorCode.INVALID_REQUEST_FORMAT]: 'Request format is invalid', [ErrorCode.DATABASE_ERROR]: 'A database error occurred', [ErrorCode.FILE_SYSTEM_ERROR]: 'A file system error occurred', [ErrorCode.INTERNAL_SERVER_ERROR]: 'An internal server error occurred', [ErrorCode.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable', [ErrorCode.QUIZ_CREATION_ERROR]: 'Failed to create quiz', [ErrorCode.QUIZ_RETRIEVAL_ERROR]: 'Failed to retrieve quiz', }; // Logger utility class Logger { static error(message, error, correlationId) { const logEntry = { level: 'error', message, timestamp: new Date().toISOString(), correlationId: correlationId || (error instanceof AppError ? error.correlationId : (0, uuid_1.v4)()), error: { name: error.name, code: error instanceof AppError ? error.code : 'UNKNOWN_ERROR', message: error.message, stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, }, }; console.error(JSON.stringify(logEntry)); } static warn(message, context) { if (this.shouldLog('warn')) { console.warn(JSON.stringify({ level: 'warn', message, timestamp: new Date().toISOString(), ...context, })); } } static info(message, context) { if (this.shouldLog('info')) { console.log(JSON.stringify({ level: 'info', message, timestamp: new Date().toISOString(), ...context, })); } } static shouldLog(level) { const levels = ['error', 'warn', 'info', 'debug']; return levels.indexOf(level) <= levels.indexOf(this.logLevel); } } exports.Logger = Logger; Logger.logLevel = process.env.LOG_LEVEL || 'info'; // Error factory functions const createValidationError = (message, details) => new AppError(ErrorCode.VALIDATION_ERROR, message, 400, details); exports.createValidationError = createValidationError; const createNotFoundError = (resource) => new AppError(ErrorCode.QUIZ_NOT_FOUND, `${resource} not found`, 404); exports.createNotFoundError = createNotFoundError; const createDatabaseError = (operation, originalError) => new AppError(ErrorCode.DATABASE_ERROR, `Database ${operation} failed`, 500, process.env.NODE_ENV === 'development' ? originalError === null || originalError === void 0 ? void 0 : originalError.message : undefined); exports.createDatabaseError = createDatabaseError; const createInternalError = (message, details) => new AppError(ErrorCode.INTERNAL_SERVER_ERROR, message, 500, details); exports.createInternalError = createInternalError; // Express error handling middleware const errorHandler = (error, req, res, next) => { let appError; if (error instanceof AppError) { appError = error; } else { // Transform unknown errors to AppError appError = new AppError(ErrorCode.INTERNAL_SERVER_ERROR, process.env.NODE_ENV === 'development' ? error.message : 'An unexpected error occurred', 500, process.env.NODE_ENV === 'development' ? error.stack : undefined); } // Log the error Logger.error(`Request failed: ${req.method} ${req.path}`, appError); // Send safe error response const errorResponse = { code: appError.code, message: process.env.NODE_ENV === 'production' ? SAFE_ERROR_MESSAGES[appError.code] || 'An error occurred' : appError.message, correlationId: appError.correlationId, timestamp: appError.timestamp, httpStatus: appError.httpStatus, }; // Include details only in development if (process.env.NODE_ENV === 'development' && appError.details) { errorResponse.details = appError.details; } res.status(appError.httpStatus).json({ error: errorResponse, }); }; exports.errorHandler = errorHandler; // Async wrapper to catch unhandled promise rejections const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; exports.asyncHandler = asyncHandler; // Validation helpers const validateRequired = (fields, requiredFields) => { const missing = requiredFields.filter(field => !fields[field]); if (missing.length > 0) { throw (0, exports.createValidationError)(`Missing required fields: ${missing.join(', ')}`, `Required fields: ${requiredFields.join(', ')}`); } }; exports.validateRequired = validateRequired; const validateQuizFormat = (quiz) => { if (!quiz || typeof quiz !== 'object') { throw (0, exports.createValidationError)('Quiz must be an object'); } (0, exports.validateRequired)(quiz, ['title', 'questions']); if (!Array.isArray(quiz.questions) || quiz.questions.length === 0) { throw (0, exports.createValidationError)('Questions must be a non-empty array'); } quiz.questions.forEach((question, index) => { if (!question || typeof question !== 'object') { throw (0, exports.createValidationError)(`Question ${index + 1} must be an object`); } (0, exports.validateRequired)(question, ['question', 'options', 'answer']); if (!Array.isArray(question.options) || question.options.length < 2) { throw (0, exports.createValidationError)(`Question ${index + 1} must have at least 2 options`); } if (!question.options.includes(question.answer)) { throw (0, exports.createValidationError)(`Question ${index + 1} answer must be one of the provided options`); } }); }; exports.validateQuizFormat = validateQuizFormat;