mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-20 09:22:49 +03:00
d8ee09d0fa
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)
346 lines
12 KiB
JavaScript
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)
|
|
};
|