refactor: Phase 2 - add error handling modules and response helpers
This commit is contained in:
85
dashcaddy-api/error-handler.js
Normal file
85
dashcaddy-api/error-handler.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Error Handler Middleware
|
||||
// Centralizes error handling logic to eliminate duplicate catch blocks
|
||||
|
||||
const { HTTP_STATUS } = require('./constants');
|
||||
|
||||
/**
|
||||
* Async route handler wrapper - catches errors and passes to error middleware
|
||||
* Usage: app.get('/route', asyncHandler(async (req, res) => { ... }))
|
||||
*/
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Express error middleware - handles all errors consistently
|
||||
*/
|
||||
function errorMiddleware(err, req, res, next) {
|
||||
const logger = req.app.get('logger');
|
||||
|
||||
// Log the error with context
|
||||
logger.error('Request error', {
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userId: req.user?.id
|
||||
});
|
||||
|
||||
// Determine status code
|
||||
const statusCode = err.statusCode || err.status || HTTP_STATUS.INTERNAL_ERROR;
|
||||
|
||||
// Send consistent error response
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: err.message || 'Internal server error',
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error classes for specific scenarios
|
||||
*/
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
this.statusCode = HTTP_STATUS.BAD_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedError extends Error {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message);
|
||||
this.name = 'UnauthorizedError';
|
||||
this.statusCode = HTTP_STATUS.UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends Error {
|
||||
constructor(message = 'Not found') {
|
||||
super(message);
|
||||
this.name = 'NotFoundError';
|
||||
this.statusCode = HTTP_STATUS.NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ConflictError';
|
||||
this.statusCode = HTTP_STATUS.CONFLICT;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
asyncHandler,
|
||||
errorMiddleware,
|
||||
ValidationError,
|
||||
UnauthorizedError,
|
||||
NotFoundError,
|
||||
ConflictError
|
||||
};
|
||||
135
dashcaddy-api/error-logger.js
Normal file
135
dashcaddy-api/error-logger.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// Error Logger Utility
|
||||
// Centralized error logging with rotation and request context tracking
|
||||
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { LIMITS } = require('./constants');
|
||||
|
||||
const ERROR_LOG_FILE = path.join(__dirname, 'error.log');
|
||||
const MAX_ERROR_LOG_SIZE = LIMITS.ERROR_LOG_SIZE;
|
||||
|
||||
/**
|
||||
* Check if file exists
|
||||
*/
|
||||
async function exists(filepath) {
|
||||
try {
|
||||
await fsp.access(filepath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error with context and rotation
|
||||
* @param {string} context - Where the error occurred
|
||||
* @param {Error|string} error - The error to log
|
||||
* @param {Object} additionalInfo - Additional context (req, etc.)
|
||||
*/
|
||||
async function logError(context, error, additionalInfo = {}) {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Extract request context if a request object is provided
|
||||
const requestContext = extractRequestContext(additionalInfo.req);
|
||||
if (additionalInfo.req) {
|
||||
delete additionalInfo.req; // Remove req to avoid circular refs
|
||||
}
|
||||
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
context,
|
||||
...requestContext,
|
||||
error: {
|
||||
message: error.message || error,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
},
|
||||
...additionalInfo
|
||||
};
|
||||
|
||||
// Format log line with request context
|
||||
const contextInfo = Object.keys(requestContext).length > 0
|
||||
? `\nRequest Context: ${JSON.stringify(requestContext, null, 2)}`
|
||||
: '';
|
||||
const logLine = `[${timestamp}] ${context}: ${error.message || error}\n${error.stack || ''}${contextInfo}\nAdditional Info: ${JSON.stringify(additionalInfo, null, 2)}\n${'='.repeat(80)}\n`;
|
||||
|
||||
try {
|
||||
// Rotate log if it exceeds max size
|
||||
await rotateLogIfNeeded();
|
||||
await fsp.appendFile(ERROR_LOG_FILE, logLine);
|
||||
} catch (e) {
|
||||
console.error('Failed to write to error log', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract request context from Express request object
|
||||
*/
|
||||
function extractRequestContext(req) {
|
||||
if (!req) return {};
|
||||
|
||||
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
||||
|
||||
return {
|
||||
requestId: req.id,
|
||||
ip: clientIP,
|
||||
userAgent: req.get('user-agent'),
|
||||
method: req.method,
|
||||
path: req.path
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate log file if it exceeds max size
|
||||
*/
|
||||
async function rotateLogIfNeeded() {
|
||||
try {
|
||||
const stats = await fsp.stat(ERROR_LOG_FILE);
|
||||
if (stats.size > MAX_ERROR_LOG_SIZE) {
|
||||
const rotated = ERROR_LOG_FILE + '.1';
|
||||
if (await exists(rotated)) {
|
||||
await fsp.unlink(rotated);
|
||||
}
|
||||
await fsp.rename(ERROR_LOG_FILE, rotated);
|
||||
}
|
||||
} catch (_) {
|
||||
// File may not exist yet, that's fine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a safe error message to the client without leaking internals
|
||||
*/
|
||||
function safeErrorMessage(error) {
|
||||
const msg = error.message || String(error);
|
||||
|
||||
// Detect port conflict errors from Docker
|
||||
const portMatch = msg.match(/exposing port TCP [^:]*:(\d+)/);
|
||||
if (portMatch || msg.includes('port is already allocated') || msg.includes('ports are not available')) {
|
||||
const port = portMatch ? portMatch[1] : 'requested';
|
||||
return `Port ${port} is already in use. Please choose a different port or stop the conflicting service.`;
|
||||
}
|
||||
|
||||
// Detect container not found errors
|
||||
if (msg.includes('No such container')) {
|
||||
return 'Container not found';
|
||||
}
|
||||
|
||||
// Detect network errors
|
||||
if (msg.includes('ECONNREFUSED') || msg.includes('ETIMEDOUT')) {
|
||||
return 'Service unavailable';
|
||||
}
|
||||
|
||||
// Generic safe message for unknown errors
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return 'An error occurred. Please try again or contact support.';
|
||||
}
|
||||
|
||||
// In development, show the actual error
|
||||
return msg;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logError,
|
||||
safeErrorMessage
|
||||
};
|
||||
114
dashcaddy-api/response-helpers.js
Normal file
114
dashcaddy-api/response-helpers.js
Normal file
@@ -0,0 +1,114 @@
|
||||
// Response Helpers
|
||||
// Standardize API response format across all routes
|
||||
|
||||
const { HTTP_STATUS } = require('./constants');
|
||||
|
||||
/**
|
||||
* Success response with data
|
||||
*/
|
||||
function success(res, data, statusCode = HTTP_STATUS.OK) {
|
||||
return res.status(statusCode).json({
|
||||
success: true,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Success response with message
|
||||
*/
|
||||
function successMessage(res, message, statusCode = HTTP_STATUS.OK) {
|
||||
return res.status(statusCode).json({
|
||||
success: true,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Created response (201)
|
||||
*/
|
||||
function created(res, data) {
|
||||
return res.status(HTTP_STATUS.CREATED).json({
|
||||
success: true,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* No content response (204)
|
||||
*/
|
||||
function noContent(res) {
|
||||
return res.status(HTTP_STATUS.NO_CONTENT).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response
|
||||
*/
|
||||
function error(res, message, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error response (400)
|
||||
*/
|
||||
function validationError(res, message) {
|
||||
return res.status(HTTP_STATUS.BAD_REQUEST).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized response (401)
|
||||
*/
|
||||
function unauthorized(res, message = 'Unauthorized') {
|
||||
return res.status(HTTP_STATUS.UNAUTHORIZED).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbidden response (403)
|
||||
*/
|
||||
function forbidden(res, message = 'Forbidden') {
|
||||
return res.status(HTTP_STATUS.FORBIDDEN).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Not found response (404)
|
||||
*/
|
||||
function notFound(res, message = 'Not found') {
|
||||
return res.status(HTTP_STATUS.NOT_FOUND).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict response (409)
|
||||
*/
|
||||
function conflict(res, message) {
|
||||
return res.status(HTTP_STATUS.CONFLICT).json({
|
||||
success: false,
|
||||
error: message
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
successMessage,
|
||||
created,
|
||||
noContent,
|
||||
error,
|
||||
validationError,
|
||||
unauthorized,
|
||||
forbidden,
|
||||
notFound,
|
||||
conflict
|
||||
};
|
||||
@@ -60,6 +60,7 @@ const auditLogger = require('./audit-logger');
|
||||
const portLockManager = require('./port-lock-manager');
|
||||
const dockerSecurity = require('./docker-security');
|
||||
const authManager = require('./auth-manager');
|
||||
const responseHelpers = require('./response-helpers');
|
||||
const configureMiddleware = require('./middleware');
|
||||
const { validateStartupConfig, syncHealthCheckerServices } = require('./startup-validator');
|
||||
const { CSRF_HEADER_NAME } = require('./csrf-protection');
|
||||
@@ -1960,3 +1961,4 @@ process.on('uncaughtException', (error) => {
|
||||
setTimeout(() => process.exit(1), 1000).unref();
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user