import { ErrorRequestHandler, Request, Response } from "express"; import createHttpError, { HttpError } from "http-errors"; import { logError, logWarn } from '../lib/logger'; import { AppLocals } from "../types/AppLocals"; import { failedRequestCounter, rejectedRequestCounter } from "../lib/metricsCounters"; import { SupportedRoutes } from "../types/enums/SupportedRoutes"; /** * Error handler that processes and formats error responses. * Handles different error types, logs appropriately, and updates metrics. * * @param err - HTTP error object * @param req - Express request object * @param res - Express response object * @param next - Express next function */ export const errorRouter:ErrorRequestHandler = async (err:HttpError, req, res, next) => { const requestPath = req.path as SupportedRoutes; // Since this error handler is complex, it might throw an error somewhere // Wrap it in try-catch to ensure it won't crash the entire process try { let errorLogText:string = err.message, errorLogName:string = err.name const responseStatus:number = err.status; let responseBody:string = "", responseContentType = "text/html"; switch(err.status) { case 400: responseBody = 'bad request'; break; case 401: responseBody = 'unauthorized'; break; case 403: responseBody = 'forbidden'; break; case 404: logWarn(`page not found ${req.method} ${requestPath}`) responseBody = 'page not found'; errorLogText = `page ${requestPath} not found`; break; case 500: responseBody = "internal server error"; errorLogText = err.message; break; default: responseBody = err.name; errorLogText = `err.status=${err.status};err.name=${err.name};err.message=${err.message}`; } logWarn(`${errorLogName}:${errorLogText}`); // `headersSent` will be TRUE if the router where the error occurred has already sent headers // If we try to set them again, it will throw an error - we can avoid that here if(!res.headersSent) { res.status(responseStatus); res.setHeader('Content-Type', responseContentType); res.end(responseBody); } else { // If `end` hasn't been called - call it to finish processing the request // Otherwise the connection will remain open until timeout if(!res.writableEnded) { res.end(); } } } catch(ex:any) { // This error will be handled by `finalErrorRouter` next(createHttpError(500, ex)); } // Prevent prometheus client from crashing the server try { switch(err.status) { case 400: case 401: case 403: case 404: // Count rejected requests separately from errors rejectedRequestCounter.inc({ path: requestPath, status: err.status }); break; case 500: default: failedRequestCounter.inc({ path: requestPath, status: err.status }); break; } (res.locals as AppLocals).stopPrometheusTimer({ path: req.path, status: err.status }); } catch(ex:any) { logError(`Error while processing prometheus metrics: ${ex.message}`); } };