diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 589d825..8fd48a2 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -1,14 +1,20 @@ +// src/middleware/errorHandler const crypto = require("crypto"); const getBaseContext = require("../utils/baseContext"); +const { getErrorContext } = require("../utils/errorContext"); +const { buildErrorRenderContext } = require("../utils/buildErrorRenderContext"); +const { isDev } = require("../utils/env"); + module.exports = async (err, req, res, next) => { const statusCode = err.statusCode ?? 500; const message = err.message ?? "Internal Server Error"; const stack = err.stack ?? "No stack trace available"; const code = err.code ?? null; const requestId = crypto.randomUUID?.() ?? Date.now().toString(36); + const timestamp = new Date().toISOString(); const logEntry = { - timestamp: new Date().toISOString(), + timestamp, level: "error", requestId, method: req.method, @@ -26,50 +32,27 @@ if (req?.log?.error) { req.log.error(logEntry); } else { - console.error(JSON.stringify(logEntry, null, 2)); + console.error(logEntry); } - const errorContextMap = { - EBADCSRFTOKEN: { - title: "Forbidden", - message: "Your request could not be processed.", - statusCode: 403, - }, - 404: { - title: "Not Found", - message: "The requested resource was not found.", - statusCode: 404, - }, - }; + const errorContext = getErrorContext(code || statusCode); - const errorContext = errorContextMap[code || statusCode] || { - title: `Error ${statusCode}`, - message: "An unexpected error occurred. Please try again later.", - statusCode, - }; - const isProd = process.env.NODE_ENV == "production"; - const context = { - title: errorContext.title, - message: isProd ? errorContext.message : message, - content: isProd - ? "" - : { - requestId, - method: req.method, - url: req.originalUrl || req.url, - statusCode, - headers: req.headers, - query: req.query, - body: req.body, - ip: req.ip || req.connection?.remoteAddress, - stack, - }, - }; - - if (process.env.NODE_ENV === "production") { + if (!isDev) { res.redirect(`/error?code=${errorContext.statusCode}`); - } else { - const errorPageContext = await getBaseContext(context); - res.status(errorContext.statusCode).render("pages/error", errorPageContext); + return; } + + const context = buildErrorRenderContext({ + req, + requestId, + timestamp, + code, + statusCode, + message, + stack, + errorContext, + }); + + const errorPageContext = await getBaseContext(context); + res.status(errorContext.statusCode).render("pages/error", errorPageContext); }; diff --git a/src/routes/errorPage.js b/src/routes/errorPage.js index a57c150..2d858a3 100644 --- a/src/routes/errorPage.js +++ b/src/routes/errorPage.js @@ -1,30 +1,17 @@ +// src/routes/errorPage const getBaseContext = require("../utils/baseContext"); +const { getErrorContext } = require("../utils/errorContext"); + module.exports = async (req, res) => { const code = parseInt(req.query.code, 10) || 500; - - const errorContextMap = { - 403: { - title: "Forbidden", - message: "Your request could not be processed.", - }, - 404: { - title: "Not Found", - message: "The requested resource was not found.", - }, - 500: { - title: "Server Error", - message: "An unexpected error occurred. Please try again later.", - }, - }; - - const errorContext = errorContextMap[code] || errorContextMap[500]; + const errorContext = getErrorContext(code); const context = await getBaseContext({ title: errorContext.title, message: errorContext.message, - statusCode: code, + statusCode: errorContext.statusCode, content: "", }); - res.status(code).render("pages/error", context); + res.status(errorContext.statusCode).render("pages/error", context); }; diff --git a/src/routes/index.js b/src/routes/index.js index bb6efd3..cc747da 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -14,14 +14,15 @@ const pages = require("./pages"); const rssFeed = require("./rssFeed"); const logs = require("./logs"); +const { isDev } = require("../utils/env"); router.get("/error", errorPage); // Landing page after error is logged router.get("/favicon.ico", (req, res) => res.status(204).end()); -if (process.env.NODE_ENV != "production") { +if (!isDev) { const logs = require("./logs"); - // router.use(logs); + router.use(logs); } router.post("/track", analytics); @@ -53,7 +54,7 @@ }); router.use((req, res, next) => { - const err = new Error("Not Found"); + const err = new Error(); err.statusCode = 404; next(err); }); diff --git a/src/routes/logs.js b/src/routes/logs.js index 201d5b3..97602ce 100644 --- a/src/routes/logs.js +++ b/src/routes/logs.js @@ -8,7 +8,7 @@ const allowedTypes = ["testing", "live", "dev"]; const dbPath = path.resolve(__dirname, "../../data/logs.sqlite3"); -console.log(dbPath); + if (!fs.existsSync(dbPath)) { // Create empty file to allow readonly open later fs.closeSync(fs.openSync(dbPath, "w")); diff --git a/src/utils/buildErrorRenderContext.js b/src/utils/buildErrorRenderContext.js new file mode 100644 index 0000000..cf74014 --- /dev/null +++ b/src/utils/buildErrorRenderContext.js @@ -0,0 +1,38 @@ +const { isProd } = require("./env"); + +function buildErrorRenderContext({ + req, + requestId, + timestamp, + code, + statusCode, + message, + stack, + errorContext, +}) { + return { + title: errorContext.title, + message: isProd ? errorContext.message : message, + content: isProd + ? "" + : JSON.stringify( + { + timestamp, + requestId, + method: req.method, + url: req.originalUrl || req.url, + code, + statusCode, + headers: req.headers, + query: req.query, + body: req.body, + ip: req.ip || req.connection?.remoteAddress, + stack, + }, + null, + 2 + ), + }; +} + +module.exports = { buildErrorRenderContext }; diff --git a/src/utils/env.js b/src/utils/env.js new file mode 100644 index 0000000..7af3c4a --- /dev/null +++ b/src/utils/env.js @@ -0,0 +1,9 @@ +const NODE_ENV = process.env.NODE_ENV || "development"; +const isProd = NODE_ENV === "production"; +const isDev = NODE_ENV === "development"; + +module.exports = { + NODE_ENV, + isProd, + isDev, +}; diff --git a/src/utils/errorContext.js b/src/utils/errorContext.js new file mode 100644 index 0000000..cbf296a --- /dev/null +++ b/src/utils/errorContext.js @@ -0,0 +1,40 @@ +const DEFAULT_STATUS = 500; + +const codeMap = { + 403: { + title: "Forbidden", + message: "Your request could not be processed.", + }, + 404: { + title: "Not Found", + message: "The requested resource was not found.", + }, + 500: { + title: "Server Error", + message: "An unexpected error occurred. Please try again later.", + }, +}; + +const nameMap = { + EBADCSRFTOKEN: { + title: "Forbidden", + message: "Your request could not be processed.", + statusCode: 403, + }, +}; + +function getErrorContext(codeOrName) { + if (typeof codeOrName === "string" && nameMap[codeOrName]) { + return nameMap[codeOrName]; + } + + const code = parseInt(codeOrName, 10); + const context = codeMap[code] || codeMap[DEFAULT_STATUS]; + + return { + ...context, + statusCode: code || DEFAULT_STATUS, + }; +} + +module.exports = { getErrorContext }; diff --git a/src/utils/logging.js b/src/utils/logging.js index 1d26504..3b81394 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.js @@ -164,6 +164,14 @@ level: "debug", format: format.combine(format.colorize(), format.simple()), }), + // new transports.Console({ + // level: "debug", // or "warn"/"error" + // format: format.combine( + // format.colorize(), + // format.printf(({ level, message }) => `[${level}] ${message}`) + // ), + // }), + sqliteTransport, ], }); diff --git a/src/utils/structuredLogger.js b/src/utils/structuredLogger.js index 27cbfa0..b0d4a6e 100644 --- a/src/utils/structuredLogger.js +++ b/src/utils/structuredLogger.js @@ -16,6 +16,7 @@ ) { // Flatten nested objects into key-value pairs for metadata const flatten = (obj, prefix = "") => { + if (!obj || typeof obj !== "object") return {}; const res = {}; for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k; diff --git a/src/views/pages/error.handlebars b/src/views/pages/error.handlebars index 4297203..0562fa9 100644 --- a/src/views/pages/error.handlebars +++ b/src/views/pages/error.handlebars @@ -1,3 +1,3 @@

{{title}}

{{message}}

-{{{content}}} +
{{{content}}}