Initial commit: Inventory Barcode System
This commit is contained in:
248
middleware/errorHandler.js
Normal file
248
middleware/errorHandler.js
Normal file
@ -0,0 +1,248 @@
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Error types and their corresponding HTTP status codes
|
||||
*/
|
||||
const ErrorTypes = {
|
||||
VALIDATION_ERROR: 'ValidationError',
|
||||
NOT_FOUND_ERROR: 'NotFoundError',
|
||||
CONFLICT_ERROR: 'ConflictError',
|
||||
UNAUTHORIZED_ERROR: 'UnauthorizedError',
|
||||
FORBIDDEN_ERROR: 'ForbiddenError',
|
||||
RATE_LIMIT_ERROR: 'RateLimitError',
|
||||
FILE_UPLOAD_ERROR: 'FileUploadError',
|
||||
DATABASE_ERROR: 'DatabaseError',
|
||||
EXTERNAL_SERVICE_ERROR: 'ExternalServiceError',
|
||||
BUSINESS_LOGIC_ERROR: 'BusinessLogicError'
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom error class for application-specific errors
|
||||
*/
|
||||
class AppError extends Error {
|
||||
constructor(message, type = 'ApplicationError', statusCode = 500, isOperational = true, details = null) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.type = type;
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = isOperational;
|
||||
this.details = details;
|
||||
this.timestamp = new Date().toISOString();
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create specific error types
|
||||
*/
|
||||
class ValidationError extends AppError {
|
||||
constructor(message, details = null) {
|
||||
super(message, ErrorTypes.VALIDATION_ERROR, 400, true, details);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message, details = null) {
|
||||
super(message, ErrorTypes.NOT_FOUND_ERROR, 404, true, details);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message, details = null) {
|
||||
super(message, ErrorTypes.CONFLICT_ERROR, 409, true, details);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message, details = null) {
|
||||
super(message, ErrorTypes.DATABASE_ERROR, 500, true, details);
|
||||
}
|
||||
}
|
||||
|
||||
class BusinessLogicError extends AppError {
|
||||
constructor(message, details = null) {
|
||||
super(message, ErrorTypes.BUSINESS_LOGIC_ERROR, 422, true, details);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response formatter
|
||||
*/
|
||||
const formatErrorResponse = (error, req) => {
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const baseResponse = {
|
||||
success: false,
|
||||
error: error.type || 'InternalServerError',
|
||||
message: error.message || 'An unexpected error occurred',
|
||||
timestamp: error.timestamp || new Date().toISOString(),
|
||||
path: req.originalUrl,
|
||||
method: req.method
|
||||
};
|
||||
|
||||
// Add details for operational errors
|
||||
if (error.isOperational && error.details) {
|
||||
baseResponse.details = error.details;
|
||||
}
|
||||
|
||||
// Add stack trace in development
|
||||
if (isDevelopment && error.stack) {
|
||||
baseResponse.stack = error.stack;
|
||||
}
|
||||
|
||||
// Add request ID if available
|
||||
if (req.requestId) {
|
||||
baseResponse.requestId = req.requestId;
|
||||
}
|
||||
|
||||
return baseResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if error should be retried
|
||||
*/
|
||||
const isRetryableError = (error) => {
|
||||
const retryableTypes = [
|
||||
ErrorTypes.DATABASE_ERROR,
|
||||
ErrorTypes.EXTERNAL_SERVICE_ERROR
|
||||
];
|
||||
|
||||
const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
|
||||
|
||||
return retryableTypes.includes(error.type) ||
|
||||
retryableCodes.includes(error.code) ||
|
||||
(error.statusCode >= 500 && error.statusCode < 600);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user-friendly error message
|
||||
*/
|
||||
const getUserFriendlyMessage = (error) => {
|
||||
const friendlyMessages = {
|
||||
[ErrorTypes.VALIDATION_ERROR]: 'The provided data is invalid. Please check your input and try again.',
|
||||
[ErrorTypes.NOT_FOUND_ERROR]: 'The requested resource could not be found.',
|
||||
[ErrorTypes.CONFLICT_ERROR]: 'This operation conflicts with existing data. Please check for duplicates.',
|
||||
[ErrorTypes.UNAUTHORIZED_ERROR]: 'Authentication is required to access this resource.',
|
||||
[ErrorTypes.FORBIDDEN_ERROR]: 'You do not have permission to perform this action.',
|
||||
[ErrorTypes.RATE_LIMIT_ERROR]: 'Too many requests. Please wait a moment before trying again.',
|
||||
[ErrorTypes.FILE_UPLOAD_ERROR]: 'There was a problem with the uploaded file. Please check the file format and size.',
|
||||
[ErrorTypes.DATABASE_ERROR]: 'A database error occurred. Please try again later.',
|
||||
[ErrorTypes.EXTERNAL_SERVICE_ERROR]: 'An external service is temporarily unavailable. Please try again later.',
|
||||
[ErrorTypes.BUSINESS_LOGIC_ERROR]: 'This operation cannot be completed due to business rules.'
|
||||
};
|
||||
|
||||
return friendlyMessages[error.type] || error.message || 'An unexpected error occurred. Please try again later.';
|
||||
};
|
||||
|
||||
/**
|
||||
* Main error handling middleware
|
||||
*/
|
||||
const errorHandler = (error, req, res, next) => {
|
||||
// Log the error with context
|
||||
const errorContext = {
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
body: req.method !== 'GET' ? req.body : undefined,
|
||||
params: req.params,
|
||||
query: req.query,
|
||||
requestId: req.requestId
|
||||
};
|
||||
|
||||
logger.logError(error, errorContext);
|
||||
|
||||
// Handle specific error types
|
||||
let appError = error;
|
||||
|
||||
// Convert known errors to AppError instances
|
||||
if (error.name === 'ValidationError') {
|
||||
appError = new ValidationError(error.message, error.details);
|
||||
} else if (error.name === 'CastError') {
|
||||
appError = new ValidationError('Invalid data format provided');
|
||||
} else if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
appError = new ConflictError('A record with this information already exists');
|
||||
} else if (error.code === 'SQLITE_CONSTRAINT') {
|
||||
appError = new ValidationError('Data constraint violation');
|
||||
} else if (error.code === 'ENOENT') {
|
||||
appError = new NotFoundError('Requested file or resource not found');
|
||||
} else if (error.code === 'LIMIT_FILE_SIZE') {
|
||||
appError = new ValidationError('File size exceeds the maximum allowed limit');
|
||||
} else if (error.code === 'LIMIT_UNEXPECTED_FILE') {
|
||||
appError = new ValidationError('Unexpected file upload');
|
||||
} else if (!error.isOperational) {
|
||||
// Convert unknown errors to generic AppError
|
||||
appError = new AppError(
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'An unexpected error occurred'
|
||||
: error.message,
|
||||
'InternalServerError',
|
||||
500,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Format response
|
||||
const errorResponse = formatErrorResponse(appError, req);
|
||||
|
||||
// Override message with user-friendly version for production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
errorResponse.message = getUserFriendlyMessage(appError);
|
||||
}
|
||||
|
||||
// Add retry information for retryable errors
|
||||
if (isRetryableError(appError)) {
|
||||
errorResponse.retryable = true;
|
||||
errorResponse.retryAfter = 5; // seconds
|
||||
}
|
||||
|
||||
// Send error response
|
||||
res.status(appError.statusCode || 500).json(errorResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 404 handler for unmatched routes
|
||||
*/
|
||||
const notFoundHandler = (req, res, next) => {
|
||||
const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Async error wrapper to catch async errors in route handlers
|
||||
*/
|
||||
const asyncHandler = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request timeout handler
|
||||
*/
|
||||
const timeoutHandler = (timeout = 30000) => {
|
||||
return (req, res, next) => {
|
||||
req.setTimeout(timeout, () => {
|
||||
const error = new AppError('Request timeout', 'TimeoutError', 408);
|
||||
next(error);
|
||||
});
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ErrorTypes,
|
||||
AppError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
DatabaseError,
|
||||
BusinessLogicError,
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
asyncHandler,
|
||||
timeoutHandler,
|
||||
isRetryableError,
|
||||
getUserFriendlyMessage
|
||||
};
|
||||
Reference in New Issue
Block a user