0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added user friendly error messages to Admin API

refs #10438

- Adds new fields to errors returned from API:  help, code, and id
- Makes `message` more descriptive towards non technical users
This commit is contained in:
Nazar Gargol 2019-02-23 15:49:20 +07:00 committed by Naz Gargol
parent 4db2eb7ce1
commit 50ea7f0eff
5 changed files with 102 additions and 3 deletions

View file

@ -79,7 +79,8 @@ const http = (apiImpl, apiType) => {
debug('json response');
res.json(result || {});
})
.catch((err) => {
.catch(({err, docName, method}) => {
req.frameOptions = {docName, method};
next(err);
});
};

View file

@ -156,6 +156,9 @@ const pipeline = (apiController, apiUtils) => {
})
.then(() => {
return frame.response;
})
.catch((err) => {
throw {err, docName, method};
});
};

View file

@ -28,6 +28,11 @@
},
"clients": {
"clientNotFound": "Client not found"
},
"actions": {
"upload": {
"image": "upload image"
}
}
}
},
@ -472,6 +477,29 @@
"oembed": {
"noUrlProvided": "No url provided.",
"unknownProvider": "No provider found for supplied URL."
},
"userMessages": {
"InternalServerError": "Internal server error, cannot {action}.",
"IncorrectUsageError": "Incorrect usage error, cannot {action}.",
"NotFoundError": "Resource not found error, cannot {action}.",
"BadRequestError": "Request not understood error, cannot {action}.",
"UnauthorizedError": "Authorisation error, cannot {action}.",
"NoPermissionError": "Permission error, cannot {action}.",
"ValidationError": "Validation error, cannot {action}.",
"UnsupportedMediaTypeError": "Unsupported media error, cannot {action}.",
"TooManyRequestsError": "Too many requests error, cannot {action}.",
"MaintenanceError": "Server down for maintenance, cannot {action}.",
"MethodNotAllowedError": "Method not allowed, cannot {action}.",
"RequestEntityTooLargeError": "Request too large, cannot {action}.",
"TokenRevocationError": "Token is not available, cannot {action}.",
"VersionMismatchError": "Version mismatch error, cannot {action}.",
"DataExportError": "Error exporting content.",
"DataImportError": "Duplicated entry, cannot save {action}.",
"DatabaseVersionError": "Database version compatibility error, cannot {action}.",
"EmailError": "Error sending email!",
"ThemeValidationError": "Theme validation error, cannot {action}.",
"DisabledFeatureError": "Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.",
"UpdateCollisionError": "Saving failed! Someone else is editing this post."
}
},
"data": {

View file

@ -33,7 +33,7 @@ module.exports = function setupApiApp() {
// API error handling
apiApp.use(shared.middlewares.errorHandler.resourceNotFound);
apiApp.use(shared.middlewares.errorHandler.handleJSONResponse);
apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2);
debug('Admin API v2 setup end');

View file

@ -1,4 +1,5 @@
const hbs = require('express-hbs');
const _ = require('lodash');
const debug = require('ghost-ignition').debug('error-handler');
const config = require('../../../config');
const common = require('../../../lib/common');
@ -62,7 +63,6 @@ _private.prepareError = (err, req, res, next) => {
};
_private.JSONErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars
// @TODO: jsonapi errors format (http://jsonapi.org/format/#error-objects)
res.json({
errors: [{
message: err.message,
@ -73,6 +73,66 @@ _private.JSONErrorRenderer = (err, req, res, next) => { // eslint-disable-line n
});
};
_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 (common.i18n.doesTranslationKeyExist(`common.api.actions.${docName}.${method}`)) {
action = common.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}`;
}
userError.message = common.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,
help: err.help || null,
code: err.code || null,
id: err.id || null
}]
});
};
_private.ErrorFallbackMessage = err => `<h1>${common.i18n.t('errors.errors.oopsErrorTemplateHasError')}</h1>
<p>${common.i18n.t('errors.errors.encounteredError')}</p>
<pre>${escapeExpression(err.message || err)}</pre>
@ -180,6 +240,13 @@ errorHandler.handleJSONResponse = [
_private.JSONErrorRenderer
];
errorHandler.handleJSONResponseV2 = [
// Make sure the error can be served
_private.prepareError,
// Render the error using JSON format
_private.JSONErrorRendererV2
];
errorHandler.handleHTMLResponse = [
// Make sure the error can be served
_private.prepareError,