From 4ef7c974a37f281067dd336ab324f43ecf2728a8 Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Tue, 7 Dec 2021 18:40:46 +0000 Subject: [PATCH] Add @tryghost/mw-error-handler refs: https://github.com/TryGhost/Toolbox/issues/137 Package includes same logic as was in the Ghost codebase but needs Sentry injected --- ghost/mw-error-handler/.eslintrc.js | 6 + ghost/mw-error-handler/LICENSE | 21 ++ ghost/mw-error-handler/README.md | 39 ++++ ghost/mw-error-handler/index.js | 1 + .../mw-error-handler/lib/mw-error-handler.js | 198 ++++++++++++++++++ ghost/mw-error-handler/package.json | 31 +++ ghost/mw-error-handler/test/.eslintrc.js | 6 + .../test/error-handler.test.js | 10 + .../mw-error-handler/test/utils/assertions.js | 11 + ghost/mw-error-handler/test/utils/index.js | 11 + .../mw-error-handler/test/utils/overrides.js | 10 + 11 files changed, 344 insertions(+) create mode 100644 ghost/mw-error-handler/.eslintrc.js create mode 100644 ghost/mw-error-handler/LICENSE create mode 100644 ghost/mw-error-handler/README.md create mode 100644 ghost/mw-error-handler/index.js create mode 100644 ghost/mw-error-handler/lib/mw-error-handler.js create mode 100644 ghost/mw-error-handler/package.json create mode 100644 ghost/mw-error-handler/test/.eslintrc.js create mode 100644 ghost/mw-error-handler/test/error-handler.test.js create mode 100644 ghost/mw-error-handler/test/utils/assertions.js create mode 100644 ghost/mw-error-handler/test/utils/index.js create mode 100644 ghost/mw-error-handler/test/utils/overrides.js diff --git a/ghost/mw-error-handler/.eslintrc.js b/ghost/mw-error-handler/.eslintrc.js new file mode 100644 index 0000000000..c9c1bcb522 --- /dev/null +++ b/ghost/mw-error-handler/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/mw-error-handler/LICENSE b/ghost/mw-error-handler/LICENSE new file mode 100644 index 0000000000..366ae5f624 --- /dev/null +++ b/ghost/mw-error-handler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2021 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ghost/mw-error-handler/README.md b/ghost/mw-error-handler/README.md new file mode 100644 index 0000000000..73d5ffd995 --- /dev/null +++ b/ghost/mw-error-handler/README.md @@ -0,0 +1,39 @@ +# Mw Error Handler + +## Install + +`npm install @tryghost/mw-error-handler --save` + +or + +`yarn add @tryghost/mw-error-handler` + + +## Usage + + +## Develop + +This is a mono repository, managed with [lerna](https://lernajs.io/). + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + +## Run + +- `yarn dev` + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + + + + +# Copyright & License + +Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). \ No newline at end of file diff --git a/ghost/mw-error-handler/index.js b/ghost/mw-error-handler/index.js new file mode 100644 index 0000000000..eb024a092f --- /dev/null +++ b/ghost/mw-error-handler/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/mw-error-handler'); diff --git a/ghost/mw-error-handler/lib/mw-error-handler.js b/ghost/mw-error-handler/lib/mw-error-handler.js new file mode 100644 index 0000000000..62ff08dade --- /dev/null +++ b/ghost/mw-error-handler/lib/mw-error-handler.js @@ -0,0 +1,198 @@ +const _ = require('lodash'); +const debug = require('@tryghost/debug')('error-handler'); +const errors = require('@tryghost/errors'); +const tpl = require('@tryghost/tpl'); + +const messages = { + pageNotFound: 'Page not found', + resourceNotFound: 'Resource not found', + actions: { + images: { + upload: 'upload image' + } + }, + userMessages: { + BookshelfRelationsError: 'Database error, cannot {action}.', + 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}.', + HostLimitError: 'Host Limit error, cannot {action}.', + DisabledFeatureError: 'Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.', + UpdateCollisionError: 'Saving failed! Someone else is editing this post.' + } +}; + +/** + * Get an error ready to be shown the the user + */ +module.exports.prepareError = (err, req, res, next) => { + debug(err); + + if (Array.isArray(err)) { + err = err[0]; + } + + if (!errors.utils.isGhostError(err)) { + // We need a special case for 404 errors + 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.InternalServerError({ + 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; + + err = err.prepareErrorForUser(); + + // 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); +}; + +const jsonErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-unused-vars + res.json({ + errors: [{ + message: err.message, + context: err.context, + help: err.help, + errorType: err.errorType, + errorDetails: err.errorDetails, + ghostErrorCode: err.ghostErrorCode + }] + }); +}; + +const jsonErrorRendererV2 = (err, req, res, next) => { // eslint-disable-line no-unused-vars + const userError = 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 + }] + }); +}; + +const 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 (_.get(messages.actions, [docName, method])) { + action = tpl(messages.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 = tpl(messages.userMessages[err.name], {action: action}); + } + } + + return userError; +}; + +module.exports.resourceNotFound = (req, res, next) => { + next(new errors.NotFoundError({message: tpl(messages.resourceNotFound)})); +}; + +module.exports.pageNotFound = (req, res, next) => { + next(new errors.NotFoundError({message: tpl(messages.pageNotFound)})); +}; + +module.exports.handleJSONResponse = sentry => [ + // Make sure the error can be served + module.exports.prepareError, + // Handle the error in Sentry + sentry.errorHandler, + // Render the error using JSON format + jsonErrorRenderer +]; + +module.exports.handleJSONResponseV2 = sentry => [ + // Make sure the error can be served + module.exports.prepareError, + // Handle the error in Sentry + sentry.errorHandler, + // Render the error using JSON format + jsonErrorRendererV2 +]; + +module.exports.handleHTMLResponse = sentry => [ + // Make sure the error can be served + module.exports.prepareError, + // Handle the error in Sentry + sentry.errorHandler +]; + diff --git a/ghost/mw-error-handler/package.json b/ghost/mw-error-handler/package.json new file mode 100644 index 0000000000..d9adc4442a --- /dev/null +++ b/ghost/mw-error-handler/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tryghost/mw-error-handler", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Utils/tree/main/packages/mw-error-handler", + "author": "Ghost Foundation", + "license": "MIT", + "main": "index.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing c8 --check-coverage mocha './test/**/*.test.js'", + "lint": "eslint . --ext .js --cache", + "posttest": "yarn lint" + }, + "files": [ + "index.js", + "lib" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "c8": "7.10.0", + "mocha": "9.1.3", + "should": "13.2.3", + "sinon": "12.0.1" + }, + "dependencies": { + "@tryghost/debug": "^0.1.9", + "@tryghost/tpl": "^0.1.8" + } +} diff --git a/ghost/mw-error-handler/test/.eslintrc.js b/ghost/mw-error-handler/test/.eslintrc.js new file mode 100644 index 0000000000..829b601eb0 --- /dev/null +++ b/ghost/mw-error-handler/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/mw-error-handler/test/error-handler.test.js b/ghost/mw-error-handler/test/error-handler.test.js new file mode 100644 index 0000000000..85d69d1e08 --- /dev/null +++ b/ghost/mw-error-handler/test/error-handler.test.js @@ -0,0 +1,10 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +describe('Hello world', function () { + it('Runs a test', function () { + // TODO: Write me! + 'hello'.should.eql('hello'); + }); +}); diff --git a/ghost/mw-error-handler/test/utils/assertions.js b/ghost/mw-error-handler/test/utils/assertions.js new file mode 100644 index 0000000000..7364ee8aa1 --- /dev/null +++ b/ghost/mw-error-handler/test/utils/assertions.js @@ -0,0 +1,11 @@ +/** + * Custom Should Assertions + * + * Add any custom assertions to this file. + */ + +// Example Assertion +// should.Assertion.add('ExampleAssertion', function () { +// this.params = {operator: 'to be a valid Example Assertion'}; +// this.obj.should.be.an.Object; +// }); diff --git a/ghost/mw-error-handler/test/utils/index.js b/ghost/mw-error-handler/test/utils/index.js new file mode 100644 index 0000000000..0d67d86ff8 --- /dev/null +++ b/ghost/mw-error-handler/test/utils/index.js @@ -0,0 +1,11 @@ +/** + * Test Utilities + * + * Shared utils for writing tests + */ + +// Require overrides - these add globals for tests +require('./overrides'); + +// Require assertions - adds custom should assertions +require('./assertions'); diff --git a/ghost/mw-error-handler/test/utils/overrides.js b/ghost/mw-error-handler/test/utils/overrides.js new file mode 100644 index 0000000000..90203424ee --- /dev/null +++ b/ghost/mw-error-handler/test/utils/overrides.js @@ -0,0 +1,10 @@ +// This file is required before any test is run + +// Taken from the should wiki, this is how to make should global +// Should is a global in our eslint test config +global.should = require('should').noConflict(); +should.extend(); + +// Sinon is a simple case +// Sinon is a global in our eslint test config +global.sinon = require('sinon');