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 portLockManager = require('./port-lock-manager');
|
||||||
const dockerSecurity = require('./docker-security');
|
const dockerSecurity = require('./docker-security');
|
||||||
const authManager = require('./auth-manager');
|
const authManager = require('./auth-manager');
|
||||||
|
const responseHelpers = require('./response-helpers');
|
||||||
const configureMiddleware = require('./middleware');
|
const configureMiddleware = require('./middleware');
|
||||||
const { validateStartupConfig, syncHealthCheckerServices } = require('./startup-validator');
|
const { validateStartupConfig, syncHealthCheckerServices } = require('./startup-validator');
|
||||||
const { CSRF_HEADER_NAME } = require('./csrf-protection');
|
const { CSRF_HEADER_NAME } = require('./csrf-protection');
|
||||||
@@ -1960,3 +1961,4 @@ process.on('uncaughtException', (error) => {
|
|||||||
setTimeout(() => process.exit(1), 1000).unref();
|
setTimeout(() => process.exit(1), 1000).unref();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user