@reflet/express
Version:
Well-defined and well-typed express decorators
169 lines (168 loc) • 5.91 kB
JavaScript
;
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;
}
}