refactor: Phase 2 - add error handling modules and response helpers

This commit is contained in:
2026-03-28 19:01:24 -07:00
parent 6c3848102b
commit cc8073256a
4 changed files with 336 additions and 0 deletions

View 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
};