Ghost/ghost/errors/lib/errors.js
Sam Lord d8ee09d0fa @tryghost/errors Add stack related functions
refs: https://github.com/TryGhost/Toolbox/issues/147

Correctly prepares the stack when wrapping another error, and adds a new method on errors to create an error which can be shown to the user (i.e. remove the stack trace in production)
2021-12-01 17:28:42 +00:00

346 lines
12 KiB
JavaScript

const uuid = require('uuid');
const merge = require('lodash/merge');
const isString = require('lodash/isString');
const cloneDeep = require('lodash/cloneDeep');
const utils = require('./utils');
class GhostError extends Error {
constructor(options) {
options = options || {};
super();
/**
* defaults
*/
this.statusCode = 500;
this.errorType = 'InternalServerError';
this.level = 'normal';
this.message = 'The server has encountered an error.';
this.id = uuid.v1();
/**
* custom overrides
*/
this.id = options.id || this.id;
this.statusCode = options.statusCode || this.statusCode;
this.level = options.level || this.level;
this.context = options.context || this.context;
this.help = options.help || this.help;
this.errorType = this.name = options.errorType || this.errorType;
this.errorDetails = options.errorDetails;
// @ts-ignore
this.code = options.code || null;
this.property = options.property || null;
this.redirect = options.redirect || null;
this.message = options.message || this.message;
this.hideStack = options.hideStack;
// NOTE: Error to inherit from, override!
// Nested objects are getting copied over in one piece (can be changed, but not needed right now)
if (options.err) {
// CASE: Support err as string (it happens that third party libs return a string instead of an error instance)
if (isString(options.err)) {
/* eslint-disable no-restricted-syntax */
options.err = new Error(options.err);
/* eslint-enable no-restricted-syntax */
}
Object.getOwnPropertyNames(options.err).forEach((property) => {
if (['errorType', 'name', 'statusCode', 'message', 'level'].indexOf(property) !== -1) {
return;
}
// CASE: `code` should put options as priority over err
if (property === 'code') {
// @ts-ignore
this[property] = this[property] || options.err[property];
return;
}
if (property === 'stack') {
if (this.hideStack) {
return;
}
this[property] = utils.wrapStack(this, options.err);
return;
}
this[property] = options.err[property] || this[property];
});
}
}
prepareErrorForUser() {
const error = cloneDeep(this);
let stackbits = error.stack.split(/\n/);
// We build this up backwards, so we always insert at position 1
if (process.env.NODE_ENV === 'production') {
stackbits.splice(1, stackbits.length - 1);
} else {
// Clearly mark the strack trace
stackbits.splice(1, 0, `Stack Trace:`);
}
// Add in our custom cotext and help methods
if (this.help) {
stackbits.splice(1, 0, `${this.help}`);
}
if (this.context) {
stackbits.splice(1, 0, `${this.context}`);
}
error.stack = stackbits.join('\n');
return error;
}
}
const ghostErrors = {
InternalServerError: class InternalServerError extends GhostError {
constructor(options) {
super(merge({
statusCode: 500,
level: 'critical',
errorType: 'InternalServerError',
message: 'The server has encountered an error.'
}, options));
}
},
IncorrectUsageError: class IncorrectUsageError extends GhostError {
constructor(options) {
super(merge({
statusCode: 400,
level: 'critical',
errorType: 'IncorrectUsageError',
message: 'We detected a misuse. Please read the stack trace.'
}, options));
}
},
NotFoundError: class NotFoundError extends GhostError {
constructor(options) {
super(merge({
statusCode: 404,
errorType: 'NotFoundError',
message: 'Resource could not be found.',
hideStack: true
}, options));
}
},
BadRequestError: class BadRequestError extends GhostError {
constructor(options) {
super(merge({
statusCode: 400,
errorType: 'BadRequestError',
message: 'The request could not be understood.'
}, options));
}
},
UnauthorizedError: class UnauthorizedError extends GhostError {
constructor(options) {
super(merge({
statusCode: 401,
errorType: 'UnauthorizedError',
message: 'You are not authorised to make this request.'
}, options));
}
},
NoPermissionError: class NoPermissionError extends GhostError {
constructor(options) {
super(merge({
statusCode: 403,
errorType: 'NoPermissionError',
message: 'You do not have permission to perform this request.'
}, options));
}
},
ValidationError: class ValidationError extends GhostError {
constructor(options) {
super(merge({
statusCode: 422,
errorType: 'ValidationError',
message: 'The request failed validation.'
}, options));
}
},
UnsupportedMediaTypeError: class UnsupportedMediaTypeError extends GhostError {
constructor(options) {
super(merge({
statusCode: 415,
errorType: 'UnsupportedMediaTypeError',
message: 'The media in the request is not supported by the server.'
}, options));
}
},
TooManyRequestsError: class TooManyRequestsError extends GhostError {
constructor(options) {
super(merge({
statusCode: 429,
errorType: 'TooManyRequestsError',
message: 'Server has received too many similar requests in a short space of time.'
}, options));
}
},
MaintenanceError: class MaintenanceError extends GhostError {
constructor(options) {
super(merge({
statusCode: 503,
errorType: 'MaintenanceError',
message: 'The server is temporarily down for maintenance.'
}, options));
}
},
MethodNotAllowedError: class MethodNotAllowedError extends GhostError {
constructor(options) {
super(merge({
statusCode: 405,
errorType: 'MethodNotAllowedError',
message: 'Method not allowed for resource.'
}, options));
}
},
RequestEntityTooLargeError: class RequestEntityTooLargeError extends GhostError {
constructor(options) {
super(merge({
statusCode: 413,
errorType: 'RequestEntityTooLargeError',
message: 'Request was too big for the server to handle.'
}, options));
}
},
TokenRevocationError: class TokenRevocationError extends GhostError {
constructor(options) {
super(merge({
statusCode: 503,
errorType: 'TokenRevocationError',
message: 'Token is no longer available.'
}, options));
}
},
VersionMismatchError: class VersionMismatchError extends GhostError {
constructor(options) {
super(merge({
statusCode: 400,
errorType: 'VersionMismatchError',
message: 'Requested version does not match server version.'
}, options));
}
},
DataExportError: class DataExportError extends GhostError {
constructor(options) {
super(merge({
statusCode: 500,
errorType: 'DataExportError'
}, options));
}
},
DataImportError: class DataImportError extends GhostError {
constructor(options) {
super(merge({
statusCode: 500,
errorType: 'DataImportError'
}, options));
}
},
DatabaseVersionError: class DatabaseVersionError extends GhostError {
constructor(options) {
super(merge({
hideStack: true,
statusCode: 500,
errorType: 'DatabaseVersionError'
}, options));
}
},
EmailError: class EmailError extends GhostError {
constructor(options) {
super(merge({
statusCode: 500,
errorType: 'EmailError'
}, options));
}
},
ThemeValidationError: class ThemeValidationError extends GhostError {
constructor(options) {
super(merge({
statusCode: 422,
errorType: 'ThemeValidationError',
errorDetails: {}
}, options));
}
},
DisabledFeatureError: class DisabledFeatureError extends GhostError {
constructor(options) {
super(merge({
statusCode: 409,
errorType: 'DisabledFeatureError'
}, options));
}
},
UpdateCollisionError: class UpdateCollisionError extends GhostError {
constructor(options) {
super(merge({
statusCode: 409,
errorType: 'UpdateCollisionError'
}, options));
}
},
HostLimitError: class HostLimitError extends GhostError {
constructor(options) {
super(merge({
errorType: 'HostLimitError',
hideStack: true,
statusCode: 403
}, options));
}
},
HelperWarning: class HelperWarning extends GhostError {
constructor(options) {
super(merge({
errorType: 'HelperWarning',
hideStack: true
}, options));
}
},
PasswordResetRequiredError: class PasswordResetRequiredError extends GhostError {
constructor(options) {
super(merge({
errorType: 'PasswordResetRequiredError',
statusCode: 401,
message: 'For security, you need to create a new password. An email has been sent to you with instructions!'
}, options));
}
},
UnhandledJobError: class UnhandledJobError extends GhostError {
constructor(options) {
super(merge({
errorType: 'UnhandledJobError',
message: 'Processed job threw an unhandled error',
level: 'critical'
}, options));
}
},
NoContentError: class NoContentError extends GhostError {
constructor(options) {
super(merge({
errorType: 'NoContentError',
statusCode: 204,
hideStack: true
}, options));
}
}
};
module.exports = ghostErrors;
const ghostErrorsWithBase = Object.assign({}, ghostErrors, {GhostError});
module.exports.utils = {
serialize: utils.serialize.bind(ghostErrorsWithBase),
deserialize: utils.deserialize.bind(ghostErrorsWithBase),
isGhostError: utils.isGhostError.bind(ghostErrorsWithBase)
};