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
JavaScript
;
/**
* @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;