From 50ea7f0eff86d0a74054406b607dead2d78940d8 Mon Sep 17 00:00:00 2001 From: Nazar Gargol Date: Sat, 23 Feb 2019 15:49:20 +0700 Subject: [PATCH] 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 --- core/server/api/shared/http.js | 3 +- core/server/api/shared/pipeline.js | 3 + core/server/translations/en.json | 28 ++++++++ core/server/web/api/v2/admin/app.js | 2 +- .../web/shared/middlewares/error-handler.js | 69 ++++++++++++++++++- 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/core/server/api/shared/http.js b/core/server/api/shared/http.js index da0a5584d1..21f5e67900 100644 --- a/core/server/api/shared/http.js +++ b/core/server/api/shared/http.js @@ -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); }); }; diff --git a/core/server/api/shared/pipeline.js b/core/server/api/shared/pipeline.js index f01736673c..38803d0087 100644 --- a/core/server/api/shared/pipeline.js +++ b/core/server/api/shared/pipeline.js @@ -156,6 +156,9 @@ const pipeline = (apiController, apiUtils) => { }) .then(() => { return frame.response; + }) + .catch((err) => { + throw {err, docName, method}; }); }; diff --git a/core/server/translations/en.json b/core/server/translations/en.json index cea78c9d18..6a8eba7de1 100644 --- a/core/server/translations/en.json +++ b/core/server/translations/en.json @@ -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": { diff --git a/core/server/web/api/v2/admin/app.js b/core/server/web/api/v2/admin/app.js index 06c7d93916..d46571e22d 100644 --- a/core/server/web/api/v2/admin/app.js +++ b/core/server/web/api/v2/admin/app.js @@ -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'); diff --git a/core/server/web/shared/middlewares/error-handler.js b/core/server/web/shared/middlewares/error-handler.js index 4957762011..31fbe4a6bd 100644 --- a/core/server/web/shared/middlewares/error-handler.js +++ b/core/server/web/shared/middlewares/error-handler.js @@ -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 => `

${common.i18n.t('errors.errors.oopsErrorTemplateHasError')}

${common.i18n.t('errors.errors.encounteredError')}

${escapeExpression(err.message || err)}
@@ -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,