refactor(utils): Extract utilities from server.js
- Create src/utils/http.js - fetchT and HTTP helpers - Create src/utils/logging.js - Structured logging and error logging - Create src/utils/responses.js - Standard API responses - Create src/utils/async-handler.js - Async wrapper with error handling - Create src/utils/index.js - Consolidated exports Removes scattered helper functions from server.js
This commit is contained in:
30
dashcaddy-api/src/utils/async-handler.js
Normal file
30
dashcaddy-api/src/utils/async-handler.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Async handler wrapper - Eliminates try/catch boilerplate
|
||||
*/
|
||||
const { AppError } = require('../../errors');
|
||||
|
||||
/**
|
||||
* Wrap async route handlers - catches errors and logs them
|
||||
*/
|
||||
function asyncHandler(logError, fn, context) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
await fn(req, res, next);
|
||||
} catch (error) {
|
||||
// Let typed errors propagate to global error handler
|
||||
if (error instanceof AppError) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
await logError(context || req.path, error);
|
||||
|
||||
if (!res.headersSent) {
|
||||
const { errorResponse } = require('./responses');
|
||||
const { safeErrorMessage } = require('./logging');
|
||||
errorResponse(res, 500, safeErrorMessage(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { asyncHandler };
|
||||
80
dashcaddy-api/src/utils/http.js
Normal file
80
dashcaddy-api/src/utils/http.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* HTTP utilities - Fetch helpers and HTTP operations
|
||||
*/
|
||||
const http = require('http');
|
||||
const { TIMEOUTS } = require('../../constants');
|
||||
|
||||
/**
|
||||
* Fetch with automatic timeout
|
||||
* Drop-in replacement for fetch() with AbortSignal timeout
|
||||
*/
|
||||
function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
||||
// Caddy admin API rejects Node.js undici fetch - use raw http.request
|
||||
if (url.includes(':2019')) {
|
||||
return _httpFetch(url, opts, timeoutMs);
|
||||
}
|
||||
|
||||
if (!opts.signal) {
|
||||
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
||||
}
|
||||
delete opts.timeout;
|
||||
return fetch(url, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw http.request wrapper for Caddy admin API
|
||||
*/
|
||||
function _httpFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const options = {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port || 2019,
|
||||
path: parsed.pathname + parsed.search,
|
||||
method: (opts.method || 'GET').toUpperCase(),
|
||||
headers: { ...opts.headers },
|
||||
timeout: timeoutMs,
|
||||
};
|
||||
|
||||
if (opts.body) {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(opts.body);
|
||||
}
|
||||
|
||||
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
let size = 0;
|
||||
|
||||
res.on('data', chunk => {
|
||||
size += chunk.length;
|
||||
if (size > MAX_RESPONSE_SIZE) {
|
||||
res.destroy();
|
||||
reject(new Error(`Response from ${url} exceeded ${MAX_RESPONSE_SIZE} bytes`));
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
statusText: res.statusMessage,
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
text: () => Promise.resolve(data),
|
||||
headers: { get: (k) => res.headers[k.toLowerCase()] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error(`Request to ${url} timed out after ${timeoutMs}ms`));
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (opts.body) req.write(opts.body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { fetchT };
|
||||
25
dashcaddy-api/src/utils/index.js
Normal file
25
dashcaddy-api/src/utils/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Utilities index - Re-export all utility modules
|
||||
*/
|
||||
const { fetchT } = require('./http');
|
||||
const { LOG_LEVELS, createLogger, logError, safeErrorMessage } = require('./logging');
|
||||
const { errorResponse, ok } = require('./responses');
|
||||
const { asyncHandler } = require('./async-handler');
|
||||
|
||||
module.exports = {
|
||||
// HTTP
|
||||
fetchT,
|
||||
|
||||
// Logging
|
||||
LOG_LEVELS,
|
||||
createLogger,
|
||||
logError,
|
||||
safeErrorMessage,
|
||||
|
||||
// Responses
|
||||
errorResponse,
|
||||
ok,
|
||||
|
||||
// Async handling
|
||||
asyncHandler,
|
||||
};
|
||||
119
dashcaddy-api/src/utils/logging.js
Normal file
119
dashcaddy-api/src/utils/logging.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Logging utilities - Structured logging and error handling
|
||||
*/
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
|
||||
/**
|
||||
* Create a structured logger
|
||||
*/
|
||||
function createLogger(LOG_LEVEL) {
|
||||
function log(level, context, message, data = {}) {
|
||||
if (LOG_LEVELS[level] < LOG_LEVEL) return;
|
||||
|
||||
const entry = {
|
||||
t: new Date().toISOString(),
|
||||
level,
|
||||
ctx: context,
|
||||
msg: message,
|
||||
};
|
||||
|
||||
if (Object.keys(data).length) entry.data = data;
|
||||
|
||||
const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.info;
|
||||
fn(JSON.stringify(entry));
|
||||
}
|
||||
|
||||
log.info = (ctx, msg, data) => log('info', ctx, msg, data);
|
||||
log.warn = (ctx, msg, data) => log('warn', ctx, msg, data);
|
||||
log.error = (ctx, msg, data) => log('error', ctx, msg, data);
|
||||
log.debug = (ctx, msg, data) => log('debug', ctx, msg, data);
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error logging with context tracking
|
||||
*/
|
||||
async function logError(ERROR_LOG_FILE, MAX_ERROR_LOG_SIZE, context, error, additionalInfo = {}, log) {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Extract request context
|
||||
const requestContext = {};
|
||||
if (additionalInfo.req) {
|
||||
const req = additionalInfo.req;
|
||||
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
||||
requestContext.requestId = req.id;
|
||||
requestContext.ip = clientIP;
|
||||
requestContext.userAgent = req.get('user-agent');
|
||||
requestContext.method = req.method;
|
||||
requestContext.path = req.path;
|
||||
delete additionalInfo.req;
|
||||
}
|
||||
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
context,
|
||||
...requestContext,
|
||||
error: {
|
||||
message: error.message || error,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
},
|
||||
...additionalInfo
|
||||
};
|
||||
|
||||
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
|
||||
try {
|
||||
const stats = await fsp.stat(ERROR_LOG_FILE);
|
||||
if (stats.size > MAX_ERROR_LOG_SIZE) {
|
||||
const rotated = ERROR_LOG_FILE + '.1';
|
||||
const exists = await fsp.access(rotated).then(() => true).catch(() => false);
|
||||
if (exists) await fsp.unlink(rotated);
|
||||
await fsp.rename(ERROR_LOG_FILE, rotated);
|
||||
}
|
||||
} catch (_) { /* file may not exist yet */ }
|
||||
|
||||
await fsp.appendFile(ERROR_LOG_FILE, logLine);
|
||||
} catch (e) {
|
||||
if (log && log.error) {
|
||||
log.error('errorlog', 'Failed to write to error log', { error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a safe error message without leaking internals
|
||||
*/
|
||||
function safeErrorMessage(error) {
|
||||
const msg = error.message || String(error);
|
||||
|
||||
// Detect port conflict errors
|
||||
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 `[DC-200] Port ${port} is already in use. Try a different port or stop the service using that port first.`;
|
||||
}
|
||||
|
||||
// Only expose short, user-facing messages
|
||||
if (msg.length < 200 && !msg.includes('/') && !msg.includes('\\') && !msg.includes(' at ')) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
return 'An internal error occurred';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LOG_LEVELS,
|
||||
createLogger,
|
||||
logError,
|
||||
safeErrorMessage,
|
||||
};
|
||||
22
dashcaddy-api/src/utils/responses.js
Normal file
22
dashcaddy-api/src/utils/responses.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Response helpers - Standard API response formats
|
||||
*/
|
||||
|
||||
/**
|
||||
* Standard error response
|
||||
*/
|
||||
function errorResponse(res, statusCode, message, extras = {}) {
|
||||
return res.status(statusCode).json({ success: false, error: message, ...extras });
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard success response
|
||||
*/
|
||||
function ok(res, data = {}) {
|
||||
return res.json({ success: true, ...data });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errorResponse,
|
||||
ok,
|
||||
};
|
||||
Reference in New Issue
Block a user