@tryghost/mw-error-handler 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/lib/mw-error-handler.js +128 -53
- package/package.json +24 -18
- package/README.md +0 -39
package/LICENSE
CHANGED
package/lib/mw-error-handler.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const semver = require('semver');
|
|
3
4
|
const debug = require('@tryghost/debug')('error-handler');
|
|
4
5
|
const errors = require('@tryghost/errors');
|
|
5
6
|
const {prepareStackForUser} = require('@tryghost/errors').utils;
|
|
7
|
+
const {isReqResUserSpecific, cacheControlValues} = require('@tryghost/http-cache-utils');
|
|
6
8
|
const tpl = require('@tryghost/tpl');
|
|
7
9
|
|
|
8
10
|
const messages = {
|
|
11
|
+
genericError: 'An unexpected error occurred, please try again.',
|
|
12
|
+
invalidValue: 'Invalid value',
|
|
9
13
|
pageNotFound: 'Page not found',
|
|
10
14
|
resourceNotFound: 'Resource not found',
|
|
11
15
|
methodNotAcceptableVersionAhead: {
|
|
@@ -18,6 +22,7 @@ const messages = {
|
|
|
18
22
|
context: 'Provided client accept-version {acceptVersion} is behind current Ghost version {ghostVersion}.',
|
|
19
23
|
help: 'Try upgrading your Ghost API client.'
|
|
20
24
|
},
|
|
25
|
+
badVersion: 'Requested version is not supported.',
|
|
21
26
|
actions: {
|
|
22
27
|
images: {
|
|
23
28
|
upload: 'upload image'
|
|
@@ -51,23 +56,32 @@ const messages = {
|
|
|
51
56
|
UnknownError: 'Unknown error - {name}, cannot {action}.'
|
|
52
57
|
};
|
|
53
58
|
|
|
59
|
+
function isDependencyInStack(dependency, err) {
|
|
60
|
+
const dependencyPath = path.join('node_modules', dependency);
|
|
61
|
+
|
|
62
|
+
return err?.stack?.match(dependencyPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
54
65
|
/**
|
|
55
66
|
* Get an error ready to be shown the the user
|
|
56
67
|
*/
|
|
57
|
-
module.exports.prepareError = (err, req, res, next)
|
|
68
|
+
module.exports.prepareError = function prepareError(err, req, res, next) {
|
|
58
69
|
debug(err);
|
|
59
70
|
|
|
60
71
|
if (Array.isArray(err)) {
|
|
61
72
|
err = err[0];
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
// If the error is already a GhostError, it has been handled and can be returned as-is
|
|
76
|
+
// For everything else, we do some custom handling here
|
|
64
77
|
if (!errors.utils.isGhostError(err)) {
|
|
65
|
-
//
|
|
66
|
-
if (err.statusCode && err.statusCode === 404) {
|
|
78
|
+
// Catch bookshelf empty errors and other 404s, and turn into a Ghost 404
|
|
79
|
+
if ((err.statusCode && err.statusCode === 404) || err.message === 'EmptyResponse') {
|
|
67
80
|
err = new errors.NotFoundError({
|
|
68
81
|
err: err
|
|
69
82
|
});
|
|
70
|
-
|
|
83
|
+
// Catch handlebars / express-hbs errors, and render them as 400, rather than 500 errors as the server isn't broken
|
|
84
|
+
} else if (isDependencyInStack('handlebars', err) || isDependencyInStack('express-hbs', err)) {
|
|
71
85
|
// Temporary handling of theme errors from handlebars
|
|
72
86
|
// @TODO remove this when #10496 is solved properly
|
|
73
87
|
err = new errors.IncorrectUsageError({
|
|
@@ -75,11 +89,33 @@ module.exports.prepareError = (err, req, res, next) => {
|
|
|
75
89
|
message: err.message,
|
|
76
90
|
statusCode: err.statusCode
|
|
77
91
|
});
|
|
92
|
+
// Catch database errors and turn them into 500 errors, but log some useful data to sentry
|
|
93
|
+
} else if (isDependencyInStack('mysql2', err)) {
|
|
94
|
+
// we don't want to return raw database errors to our users
|
|
95
|
+
err.sqlErrorCode = err.code;
|
|
96
|
+
|
|
97
|
+
if (err.code === 'ER_WRONG_VALUE') {
|
|
98
|
+
err = new errors.ValidationError({
|
|
99
|
+
message: tpl(messages.invalidValue),
|
|
100
|
+
context: err.message,
|
|
101
|
+
err
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
err = new errors.InternalServerError({
|
|
105
|
+
err: err,
|
|
106
|
+
message: tpl(messages.genericError),
|
|
107
|
+
statusCode: err.statusCode,
|
|
108
|
+
code: 'UNEXPECTED_ERROR'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// For everything else, create a generic 500 error, with context set to the original error message
|
|
78
112
|
} else {
|
|
79
113
|
err = new errors.InternalServerError({
|
|
80
114
|
err: err,
|
|
81
|
-
message:
|
|
82
|
-
|
|
115
|
+
message: tpl(messages.genericError),
|
|
116
|
+
context: err.message,
|
|
117
|
+
statusCode: err.statusCode,
|
|
118
|
+
code: 'UNEXPECTED_ERROR'
|
|
83
119
|
});
|
|
84
120
|
}
|
|
85
121
|
}
|
|
@@ -90,21 +126,23 @@ module.exports.prepareError = (err, req, res, next) => {
|
|
|
90
126
|
// alternative for res.status();
|
|
91
127
|
res.statusCode = err.statusCode;
|
|
92
128
|
|
|
93
|
-
// never cache errors
|
|
94
|
-
res.set({
|
|
95
|
-
'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
|
|
96
|
-
});
|
|
97
|
-
|
|
98
129
|
next(err);
|
|
99
130
|
};
|
|
100
131
|
|
|
101
|
-
module.exports.prepareStack = (err, req, res, next)
|
|
132
|
+
module.exports.prepareStack = function prepareStack(err, req, res, next) { // eslint-disable-line no-unused-vars
|
|
102
133
|
const clonedError = prepareStackForUser(err);
|
|
103
134
|
|
|
104
135
|
next(clonedError);
|
|
105
136
|
};
|
|
106
137
|
|
|
107
|
-
|
|
138
|
+
/**
|
|
139
|
+
* @private the method is exposed for testing purposes only
|
|
140
|
+
* @param {Object} err
|
|
141
|
+
* @param {import('express').Request} req
|
|
142
|
+
* @param {import('express').Response} res
|
|
143
|
+
* @param {import('express').NextFunction} next
|
|
144
|
+
*/
|
|
145
|
+
module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, next) { // eslint-disable-line no-unused-vars
|
|
108
146
|
const userError = prepareUserMessage(err, req);
|
|
109
147
|
|
|
110
148
|
res.json({
|
|
@@ -122,7 +160,32 @@ const jsonErrorRenderer = (err, req, res, next) => { // eslint-disable-line no-u
|
|
|
122
160
|
});
|
|
123
161
|
};
|
|
124
162
|
|
|
125
|
-
|
|
163
|
+
/**
|
|
164
|
+
*
|
|
165
|
+
* @param {String} [cacheControlHeaderValue] cache-control header value
|
|
166
|
+
*/
|
|
167
|
+
module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cacheControlHeaderValue) {
|
|
168
|
+
return function prepareErrorCacheControlInner(err, req, res, next) {
|
|
169
|
+
let cacheControl = cacheControlHeaderValue;
|
|
170
|
+
if (!cacheControlHeaderValue) {
|
|
171
|
+
// never cache errors unless it's a 404
|
|
172
|
+
cacheControl = cacheControlValues.private;
|
|
173
|
+
|
|
174
|
+
// Do not include 'private' cache-control directive for 404 responses
|
|
175
|
+
if (err.statusCode === 404 && req.method === 'GET' && !isReqResUserSpecific(req, res)) {
|
|
176
|
+
cacheControl = cacheControlValues.noCacheDynamic;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
res.set({
|
|
181
|
+
'Cache-Control': cacheControl
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
next(err);
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const prepareUserMessage = function prepareUserMessage(err, req) {
|
|
126
189
|
const userError = {
|
|
127
190
|
message: err.message,
|
|
128
191
|
context: err.context
|
|
@@ -172,66 +235,78 @@ const prepareUserMessage = (err, req) => {
|
|
|
172
235
|
return userError;
|
|
173
236
|
};
|
|
174
237
|
|
|
175
|
-
module.exports.resourceNotFound = (req, res, next)
|
|
176
|
-
if (req
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let notAcceptableError;
|
|
185
|
-
if (versionComparison === 1) {
|
|
186
|
-
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
187
|
-
message: tpl(
|
|
188
|
-
messages.methodNotAcceptableVersionAhead.message
|
|
189
|
-
),
|
|
190
|
-
context: tpl(messages.methodNotAcceptableVersionAhead.context, {
|
|
191
|
-
acceptVersion: req.headers['accept-version'],
|
|
192
|
-
ghostVersion: `v${res.locals.safeVersion}`
|
|
193
|
-
}),
|
|
194
|
-
help: tpl(messages.methodNotAcceptableVersionAhead.help),
|
|
195
|
-
code: 'UPDATE_GHOST'
|
|
196
|
-
});
|
|
197
|
-
} else {
|
|
198
|
-
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
199
|
-
message: tpl(
|
|
200
|
-
messages.methodNotAcceptableVersionBehind.message
|
|
201
|
-
),
|
|
202
|
-
context: tpl(messages.methodNotAcceptableVersionBehind.context, {
|
|
203
|
-
acceptVersion: req.headers['accept-version'],
|
|
204
|
-
ghostVersion: `v${res.locals.safeVersion}`
|
|
205
|
-
}),
|
|
206
|
-
help: tpl(messages.methodNotAcceptableVersionBehind.help),
|
|
207
|
-
code: 'UPDATE_CLIENT'
|
|
208
|
-
});
|
|
238
|
+
module.exports.resourceNotFound = function resourceNotFound(req, res, next) {
|
|
239
|
+
if (req?.headers?.['accept-version'] && res.locals?.safeVersion) {
|
|
240
|
+
// Protect against invalid `Accept-Version` headers
|
|
241
|
+
const acceptVersionSemver = semver.coerce(req.headers['accept-version']);
|
|
242
|
+
if (!acceptVersionSemver) {
|
|
243
|
+
return next(new errors.BadRequestError({
|
|
244
|
+
message: tpl(messages.badVersion)
|
|
245
|
+
}));
|
|
209
246
|
}
|
|
210
247
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
248
|
+
if (semver.compare(acceptVersionSemver, semver.coerce(res.locals.safeVersion)) !== 0) {
|
|
249
|
+
const versionComparison = semver.compare(
|
|
250
|
+
acceptVersionSemver,
|
|
251
|
+
semver.coerce(res.locals.safeVersion)
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
let notAcceptableError;
|
|
255
|
+
if (versionComparison === 1) {
|
|
256
|
+
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
257
|
+
message: tpl(
|
|
258
|
+
messages.methodNotAcceptableVersionAhead.message
|
|
259
|
+
),
|
|
260
|
+
context: tpl(messages.methodNotAcceptableVersionAhead.context, {
|
|
261
|
+
acceptVersion: req.headers['accept-version'],
|
|
262
|
+
ghostVersion: `v${res.locals.safeVersion}`
|
|
263
|
+
}),
|
|
264
|
+
help: tpl(messages.methodNotAcceptableVersionAhead.help),
|
|
265
|
+
code: 'UPDATE_GHOST'
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
269
|
+
message: tpl(
|
|
270
|
+
messages.methodNotAcceptableVersionBehind.message
|
|
271
|
+
),
|
|
272
|
+
context: tpl(messages.methodNotAcceptableVersionBehind.context, {
|
|
273
|
+
acceptVersion: req.headers['accept-version'],
|
|
274
|
+
ghostVersion: `v${res.locals.safeVersion}`
|
|
275
|
+
}),
|
|
276
|
+
help: tpl(messages.methodNotAcceptableVersionBehind.help),
|
|
277
|
+
code: 'UPDATE_CLIENT'
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return next(notAcceptableError);
|
|
282
|
+
}
|
|
214
283
|
}
|
|
284
|
+
|
|
285
|
+
next(new errors.NotFoundError({message: tpl(messages.resourceNotFound)}));
|
|
215
286
|
};
|
|
216
287
|
|
|
217
|
-
module.exports.pageNotFound = (req, res, next)
|
|
288
|
+
module.exports.pageNotFound = function pageNotFound(req, res, next) {
|
|
218
289
|
next(new errors.NotFoundError({message: tpl(messages.pageNotFound)}));
|
|
219
290
|
};
|
|
220
291
|
|
|
221
292
|
module.exports.handleJSONResponse = sentry => [
|
|
222
293
|
// Make sure the error can be served
|
|
223
294
|
module.exports.prepareError,
|
|
295
|
+
// Add cache-control header
|
|
296
|
+
module.exports.prepareErrorCacheControl(),
|
|
224
297
|
// Handle the error in Sentry
|
|
225
298
|
sentry.errorHandler,
|
|
226
299
|
// Format the stack for the user
|
|
227
300
|
module.exports.prepareStack,
|
|
228
301
|
// Render the error using JSON format
|
|
229
|
-
jsonErrorRenderer
|
|
302
|
+
module.exports.jsonErrorRenderer
|
|
230
303
|
];
|
|
231
304
|
|
|
232
305
|
module.exports.handleHTMLResponse = sentry => [
|
|
233
306
|
// Make sure the error can be served
|
|
234
307
|
module.exports.prepareError,
|
|
308
|
+
// Add cache-control header
|
|
309
|
+
module.exports.prepareErrorCacheControl(cacheControlValues.private),
|
|
235
310
|
// Handle the error in Sentry
|
|
236
311
|
sentry.errorHandler,
|
|
237
312
|
// Format the stack for the user
|
package/package.json
CHANGED
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/mw-error-handler",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"repository": "https://github.com/TryGhost/Utils/tree/main/packages/mw-error-handler",
|
|
3
|
+
"version": "1.0.7",
|
|
5
4
|
"author": "Ghost Foundation",
|
|
6
5
|
"license": "MIT",
|
|
7
6
|
"main": "index.js",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
8
10
|
"scripts": {
|
|
9
11
|
"dev": "echo \"Implement me!\"",
|
|
10
|
-
"test": "NODE_ENV=testing c8 --all --check-coverage --100 --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
12
|
+
"test:unit": "NODE_ENV=testing c8 --all --check-coverage --100 --reporter text --reporter cobertura -- mocha --reporter dot './test/**/*.test.js'",
|
|
13
|
+
"test": "yarn test:unit",
|
|
14
|
+
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
|
15
|
+
"lint": "yarn lint:code && yarn lint:test",
|
|
16
|
+
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
15
19
|
"index.js",
|
|
16
20
|
"lib"
|
|
17
21
|
],
|
|
18
|
-
"publishConfig": {
|
|
19
|
-
"access": "public"
|
|
20
|
-
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"c8": "
|
|
23
|
-
"mocha": "10.
|
|
24
|
-
"
|
|
25
|
-
"sinon": "14.0.0"
|
|
23
|
+
"c8": "8.0.1",
|
|
24
|
+
"mocha": "10.8.2",
|
|
25
|
+
"sinon": "15.2.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@tryghost/debug": "^0.1.
|
|
29
|
-
"@tryghost/errors": "1.
|
|
30
|
-
"@tryghost/
|
|
31
|
-
"
|
|
32
|
-
"
|
|
28
|
+
"@tryghost/debug": "^0.1.35",
|
|
29
|
+
"@tryghost/errors": "^1.3.8",
|
|
30
|
+
"@tryghost/http-cache-utils": "^0.1.20",
|
|
31
|
+
"@tryghost/tpl": "^0.1.35",
|
|
32
|
+
"lodash": "4.17.21",
|
|
33
|
+
"semver": "7.7.2"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/TryGhost/framework.git",
|
|
38
|
+
"directory": "packages/mw-error-handler"
|
|
33
39
|
},
|
|
34
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "e395f1d79edee4aecc36e572bb402016a8ba9273"
|
|
35
41
|
}
|
package/README.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Mw Error Handler
|
|
2
|
-
|
|
3
|
-
## Install
|
|
4
|
-
|
|
5
|
-
`npm install @tryghost/mw-error-handler --save`
|
|
6
|
-
|
|
7
|
-
or
|
|
8
|
-
|
|
9
|
-
`yarn add @tryghost/mw-error-handler`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Usage
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Develop
|
|
16
|
-
|
|
17
|
-
This is a mono repository, managed with [lerna](https://lernajs.io/).
|
|
18
|
-
|
|
19
|
-
Follow the instructions for the top-level repo.
|
|
20
|
-
1. `git clone` this repo & `cd` into it as usual
|
|
21
|
-
2. Run `yarn` to install top-level dependencies.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Run
|
|
25
|
-
|
|
26
|
-
- `yarn dev`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Test
|
|
30
|
-
|
|
31
|
-
- `yarn lint` run just eslint
|
|
32
|
-
- `yarn test` run lint and tests
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Copyright & License
|
|
38
|
-
|
|
39
|
-
Copyright (c) 2013-2022 Ghost Foundation - Released under the [MIT license](LICENSE).
|