ghost
Version:
The professional publishing platform
290 lines (245 loc) • 9.53 kB
JavaScript
const hbs = require('express-hbs');
const _ = require('lodash');
const debug = require('ghost-ignition').debug('error-handler');
const errors = require('@tryghost/errors');
const config = require('../../../../shared/config');
const {i18n} = require('../../../lib/common');
const helpers = require('../../../../frontend/services/routing/helpers');
const sentry = require('../../../../shared/sentry');
const escapeExpression = hbs.Utils.escapeExpression;
const _private = {};
const errorHandler = {};
/**
* This is a bare minimum setup, which allows us to render the error page
* It uses the {{asset}} helper, and nothing more
*/
_private.createHbsEngine = () => {
const engine = hbs.create();
engine.registerHelper('asset', require('../../../../frontend/helpers/asset'));
return engine.express4();
};
/**
* Get an error ready to be shown the the user
*
* @TODO: support multiple errors within one single error, see https://github.com/TryGhost/Ghost/issues/7116#issuecomment-252231809
*/
_private.prepareError = (err, req, res, next) => {
debug(err);
if (Array.isArray(err)) {
err = err[0];
}
if (!errors.utils.isIgnitionError(err)) {
// We need a special case for 404 errors
// @TODO look at adding this to the GhostError class
if (err.statusCode && err.statusCode === 404) {
err = new errors.NotFoundError({
err: err
});
} else if (err.stack.match(/node_modules\/handlebars\//)) {
// Temporary handling of theme errors from handlebars
// @TODO remove this when #10496 is solved properly
err = new errors.IncorrectUsageError({
err: err,
message: err.message,
statusCode: err.statusCode
});
} else {
err = new errors.GhostError({
err: err,
message: err.message,
statusCode: err.statusCode
});
}
}
// used for express logging middleware see core/server/app.js
req.err = err;
// alternative for res.status();
res.statusCode = err.statusCode;
// never cache errors
res.set({
'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
});
next(err);
};
_private.JSONErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars
res.json({
errors: [{
message: err.message,
context: err.context,
errorType: err.errorType,
errorDetails: err.errorDetails
}]
});
};
_private.prepareUserMessage = (err, res) => {
const userError = {
message: err.message,
context: err.context
};
const docName = _.get(res, 'frameOptions.docName');
const method = _.get(res, 'frameOptions.method');
if (docName && method) {
let action;
const actionMap = {
browse: 'list',
read: 'read',
add: 'save',
edit: 'edit',
destroy: 'delete'
};
if (i18n.doesTranslationKeyExist(`common.api.actions.${docName}.${method}`)) {
action = i18n.t(`common.api.actions.${docName}.${method}`);
} else if (Object.keys(actionMap).includes(method)) {
let resource = docName;
if (method !== 'browse') {
resource = resource.replace(/s$/, '');
}
action = `${actionMap[method]} ${resource}`;
}
if (action) {
if (err.context) {
userError.context = `${err.message} ${err.context}`;
} else {
userError.context = err.message;
}
userError.message = i18n.t(`errors.api.userMessages.${err.name}`, {action: action});
}
}
return userError;
};
_private.JSONErrorRendererV2 = (err, req, res, next) => { // eslint-disable-line no-unused-vars
const userError = _private.prepareUserMessage(err, req);
res.json({
errors: [{
message: userError.message || null,
context: userError.context || null,
type: err.errorType || null,
details: err.errorDetails || null,
property: err.property || null,
help: err.help || null,
code: err.code || null,
id: err.id || null
}]
});
};
_private.ErrorFallbackMessage = err => `<h1>${i18n.t('errors.errors.oopsErrorTemplateHasError')}</h1>
<p>${i18n.t('errors.errors.encounteredError')}</p>
<pre>${escapeExpression(err.message || err)}</pre>
<br ><p>${i18n.t('errors.errors.whilstTryingToRender')}</p>
${err.statusCode} <pre>${escapeExpression(err.message || err)}</pre>`;
_private.ThemeErrorRenderer = (err, req, res, next) => {
// If the error code is explicitly set to STATIC_FILE_NOT_FOUND,
// Skip trying to render an HTML error, and move on to the basic error renderer
// We do this because customised 404 templates could reference the image that's missing
// A better long term solution might be to do this based on extension
if (err.code === 'STATIC_FILE_NOT_FOUND') {
return next(err);
}
// Renderer begin
// Format Data
const data = {
message: err.message,
// @deprecated Remove in Ghost 4.0
code: err.statusCode,
statusCode: err.statusCode,
errorDetails: err.errorDetails || []
};
// Template
// @TODO: very dirty !!!!!!
helpers.templates.setTemplate(req, res);
// It can be that something went wrong with the theme or otherwise loading handlebars
// This ensures that no matter what res.render will work here
// @TODO: split the error handler for assets, admin & theme to refactor this away
if (_.isEmpty(req.app.engines)) {
res._template = 'error';
req.app.engine('hbs', _private.createHbsEngine());
req.app.set('view engine', 'hbs');
req.app.set('views', config.get('paths').defaultViews);
}
// @TODO use renderer here?!
// Render Call - featuring an error handler for what happens if rendering fails
res.render(res._template, data, (_err, html) => {
if (!_err) {
return res.send(html);
}
// re-attach new error e.g. error template has syntax error or misusage
req.err = _err;
// And then try to explain things to the user...
// Cheat and output the error using handlebars escapeExpression
return res.status(500).send(_private.ErrorFallbackMessage(_err));
});
};
_private.HTMLErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars
const data = {
message: err.message,
statusCode: err.statusCode,
errorDetails: err.errorDetails || []
};
// e.g. if you serve the admin /ghost and Ghost returns a 503 because it generates the urls at the moment.
// This ensures that no matter what res.render will work here
// @TODO: put to prepare error function?
if (_.isEmpty(req.app.engines)) {
res._template = 'error';
req.app.engine('hbs', _private.createHbsEngine());
req.app.set('view engine', 'hbs');
req.app.set('views', config.get('paths').defaultViews);
}
res.render('error', data, (_err, html) => {
if (!_err) {
return res.send(html);
}
// re-attach new error e.g. error template has syntax error or misusage
req.err = _err;
// And then try to explain things to the user...
// Cheat and output the error using handlebars escapeExpression
return res.status(500).send(_private.ErrorFallbackMessage(_err));
});
};
_private.BasicErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars
return res.send(res.statusCode + ' ' + err.message);
};
errorHandler.resourceNotFound = (req, res, next) => {
// TODO, handle unknown resources & methods differently, so that we can also produce
// 405 Method Not Allowed
next(new errors.NotFoundError({message: i18n.t('errors.errors.resourceNotFound')}));
};
errorHandler.pageNotFound = (req, res, next) => {
next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
};
errorHandler.handleJSONResponse = [
// Make sure the error can be served
_private.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using JSON format
_private.JSONErrorRenderer
];
errorHandler.handleJSONResponseV2 = [
// Make sure the error can be served
_private.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using JSON format
_private.JSONErrorRendererV2
];
errorHandler.handleHTMLResponse = [
// Make sure the error can be served
_private.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using HTML format
_private.HTMLErrorRenderer,
// Fall back to basic if HTML is not explicitly accepted
_private.BasicErrorRenderer
];
errorHandler.handleThemeResponse = [
// Make sure the error can be served
_private.prepareError,
// Handle the error in Sentry
sentry.errorHandler,
// Render the error using theme template
_private.ThemeErrorRenderer,
// Fall back to basic if HTML is not explicitly accepted
_private.BasicErrorRenderer
];
module.exports = errorHandler;