UNPKG

@reflet/express

Version:

Well-defined and well-typed express decorators

169 lines (168 loc) 5.91 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.finalHandler = void 0; /** * Final error handler to apply globally on the app, to be able to send errors as `json`. * * @example * ```ts * app.use(finalHandler({ * json: true, * expose: ['name', 'message'], * log: '5xx', * notFoundHandler: true, * })) * ``` * ------ * @public */ function finalHandler(options) { const finalErrorHandler = (error, req, res, next) => { if (res.headersSent) { return next(error); } // ─── Status ─── const errorStatus = getStatusFromErrorProps(error); if (errorStatus) { res.status(errorStatus); } else if (typeof res.statusCode !== 'number' || res.statusCode < 400 || res.statusCode > 599 || Number.isNaN(res.statusCode)) { res.status(500); } // ─── Headers ─── if (!!error && !!error.headers && typeof error.headers === 'object') { res.set(error.headers); } // ─── Log ─── if (options.log) { if (typeof options.log === 'function') { options.log(error, req, res); } else if (options.log === true || (options.log === '5xx' && res.statusCode >= 500)) { setImmediate(() => console.error(error)); } } // ─── Expose ─── const marshalledError = marshalError(error, res, options.expose); // ─── Json ─── if (options.json === true) { return res.json(marshalledError); } else if (options.json === 'from-response-type') { const responseType = res.get('Content-Type'); // https://regex101.com/r/noMxut/1 const jsonInferredFromResponse = /^application\/(\S+\+|)json/m.test(responseType); if (jsonInferredFromResponse) { return res.json(marshalledError); } } else if (options.json === 'from-response-type-or-request') { const responseType = res.get('Content-Type'); const jsonInferredFromResponse = /^application\/(\S+\+|)json/m.test(responseType); const jsonInferredFromRequest = !responseType && (req.xhr || (!!req.get('Accept') && !!req.accepts('json'))); if (jsonInferredFromResponse || jsonInferredFromRequest) { return res.json(marshalledError); } } // ─── Html ─── next(marshalledError); }; if (!options.notFoundHandler) { return finalErrorHandler; } const notFoundStatus = typeof options.notFoundHandler === 'number' ? options.notFoundHandler : 404; // https://github.com/pillarjs/finalhandler/blob/v1.1.2/index.js#L113-L115 const notFoundHandlerr = typeof options.notFoundHandler === 'function' ? options.notFoundHandler : function notFoundHandler(req, res, next) { res.status(notFoundStatus); const notFoundError = new RouteNotFoundError(`Cannot ${req.method} ${req.baseUrl}${req.path}`); next(notFoundError); }; return [notFoundHandlerr, finalErrorHandler]; } exports.finalHandler = finalHandler; /** * @internal */ class RouteNotFoundError extends Error { constructor(message) { super(message); // Must assign the name this way: Object.defineProperty(this, 'name', { value: 'RouteNotFoundError', configurable: true }); Error.captureStackTrace(this, RouteNotFoundError); } // The dev user doesn't need a stack trace for this error. toString() { return `${this.name}: ${this.message}`; } } /** * @see https://github.com/pillarjs/finalhandler/blob/v1.1.2/index.js#L195-L207 * @internal */ function getStatusFromErrorProps(err) { if (!err || typeof err !== 'object') { return undefined; } if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) { return err.status; } if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) { return err.statusCode; } return undefined; } /** * @internal */ function marshalError(err, res, expose) { if (!err || typeof err !== 'object') { return err; } const exposeProps = typeof expose === 'function' ? expose(res.statusCode) : expose; if (exposeProps === true) { const obj = Object.assign({}, err); // Copy non-enumerable error properties if ('message' in err) obj.message = err.message; if ('name' in err) obj.name = err.name; if ('stack' in err) obj.stack = err.stack; if ('cause' in err) obj.cause = err.cause; // Copy serializing methods if ('toString' in err) { Object.defineProperty(obj, 'toString', { value: err.toString, enumerable: false }); } if ('toJSON' in err) { Object.defineProperty(obj, 'toJSON', { value: err.toJSON, enumerable: false }); } return obj; } else if (Array.isArray(exposeProps)) { const obj = {}; for (const prop of exposeProps) { if (prop in err) { obj[prop] = err[prop]; } } // Copy serializing methods as well if ('toString' in err) { Object.defineProperty(obj, 'toString', { value: err.toString, enumerable: false }); } if ('toJSON' in err) { Object.defineProperty(obj, 'toJSON', { value: err.toJSON, enumerable: false }); } return obj; } else { // avoid [Object object] when serializing to html const obj = Object.create({ toString: () => '' }); return obj; } }