UNPKG

veloze

Version:

A modern and fast express-like webserver for the web

170 lines (155 loc) 4.89 kB
import * as crypto from 'node:crypto' import { send } from '../response/index.js' import { escapeHtmlLit } from '../utils/index.js' import { logger } from '../utils/logger.js' import { HttpError } from '../HttpError.js' import { CONTENT_TYPE, CONTENT_SECURITY_POLICY } from '../constants.js' import { buildCsp } from './contentSec.js' /** * @typedef {import('../types.js').Request} Request * @typedef {import('../types.js').Response} Response * @typedef {import('../types.js').HttpError} HttpErrorL * @typedef {import('../types.js').Log} Log */ const log = logger(':final') /** * final handler * * provides a error response according to a given error; * returns either a json or html response; defaults to html; * logs the error together with the requests url and method; * * @param {object} [options] * @param {(param0: {status: number, message: string, info?: object, reqId: string, req: Request}) => string} [options.htmlTemplate] html template for the final error page * @returns {(err: HttpErrorL|Error, req: Request, res: Response, next?: Function) => void} */ export const finalHandler = (options) => { const { htmlTemplate = finalHtml } = options || {} const _finalLogger = finalLogger() // eslint-disable-next-line no-unused-vars return function finalHandlerMw(err, req, res, next) { // our message to the outside world /** @type {HttpError} */ // @ts-expect-error const errResp = err?.status ? err : new HttpError(500, 'Oops! That should not have happened!', err) const { status = 500, message, // PUBLIC message info // PUBLIC info: additional info, e.g. like validation error for the outside world // cause // PRIVATE cause: our internal error message and stack trace -> only for logging... } = errResp || {} if (!res.headersSent) { const reqId = (req.id = req.id || crypto.randomUUID()) const type = String(res.getHeader(CONTENT_TYPE)) const accept = String(req.headers?.accept || req.headers?.[CONTENT_TYPE]) const isJsonType = type.includes('/json') || accept.includes('/json') const body = res.body || (isJsonType ? { status, message, errors: info, reqId } : htmlTemplate({ status, message, info, reqId, req })) if (!isJsonType && !res.getHeader(CONTENT_SECURITY_POLICY)) { res.setHeader(CONTENT_SECURITY_POLICY, buildCsp()) } send(res, body, status) } else if (!res.writableEnded) { res.end() } _finalLogger(err, req, res) } } /** * Logs the error together with the requests url and method; * Don't use as a middleware. Instead call from a middleware. * @returns {(err: HttpErrorL|Error, req: Request, res: Response) => void} */ export const finalLogger = () => { return (err, req, _res) => { // @ts-expect-error const errResp = err?.status ? err : new HttpError(500, 'Oops! That should not have happened!', err) const { // @ts-expect-error status = 500, message, // PUBLIC message // info, // PUBLIC info: additional info, e.g. like validation error for the outside world cause // PRIVATE cause: our internal error message and stack trace -> only for logging... } = errResp || {} const { url, originalUrl, method, id = crypto.randomUUID() } = req // log different log-level by status code const level = status < 400 ? 'info' : status < 500 ? 'warn' : 'error' log[level]({ status, method, id, url: originalUrl || url, msg: message, // @ts-expect-error stack: cause?.stack }) } } const finalHtml = ({ status, message }) => escapeHtmlLit`<!DOCTYPE html> <html lang="en"> <head> <title>Error</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> html { box-sizing: border-box; font-size: 16px; } body, h1, h2, p { padding: 0; margin: 0; } body { font-family: sans-serif; position: relative; height: 100vh; display: flex; justify-content: center; } h1 { font-size: 10rem; font-weight: 700; margin: 0; color: transparent; background: linear-gradient(120deg, #cc33ff 30%, #33ddff); background-clip: text; -webkit-text-fill-color: transparent; -webkit-background-clip: text; } h2 { font-weight: bold; font-size: 2em; padding-bottom: 2rem; } p { font-size: 1.2em; } section { display: flex; justify-content: center; align-items: center; flex-flow: column; padding-bottom: 3em; } a { color: #0099FF; } </style> </head> <body> <section> <h1>${status}</h1> <h2>${message}</h2> <p><a href="/">Homepage</a></p> </section> </body> </html> `