@tryghost/mw-error-handler 3.0.3 → 3.2.0
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 +21 -0
- package/README.md +1 -1
- package/lib/mw-error-handler.js +67 -59
- package/package.json +23 -26
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026 Ghost Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/lib/mw-error-handler.js
CHANGED
|
@@ -3,8 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const semver = require('semver');
|
|
4
4
|
const debug = require('@tryghost/debug')('error-handler');
|
|
5
5
|
const errors = require('@tryghost/errors');
|
|
6
|
-
const {prepareStackForUser} = require('@tryghost/errors').utils;
|
|
7
|
-
const {isReqResUserSpecific, cacheControlValues} = require('@tryghost/http-cache-utils');
|
|
6
|
+
const { prepareStackForUser } = require('@tryghost/errors').utils;
|
|
7
|
+
const { isReqResUserSpecific, cacheControlValues } = require('@tryghost/http-cache-utils');
|
|
8
8
|
const tpl = require('@tryghost/tpl');
|
|
9
9
|
|
|
10
10
|
const messages = {
|
|
@@ -14,19 +14,21 @@ const messages = {
|
|
|
14
14
|
resourceNotFound: 'Resource not found',
|
|
15
15
|
methodNotAcceptableVersionAhead: {
|
|
16
16
|
message: 'Request could not be served, the endpoint was not found.',
|
|
17
|
-
context:
|
|
18
|
-
|
|
17
|
+
context:
|
|
18
|
+
'Provided client accept-version {acceptVersion} is ahead of current Ghost version {ghostVersion}.',
|
|
19
|
+
help: 'Try upgrading your Ghost install.',
|
|
19
20
|
},
|
|
20
21
|
methodNotAcceptableVersionBehind: {
|
|
21
22
|
message: 'Request could not be served, the endpoint was not found.',
|
|
22
|
-
context:
|
|
23
|
-
|
|
23
|
+
context:
|
|
24
|
+
'Provided client accept-version {acceptVersion} is behind current Ghost version {ghostVersion}.',
|
|
25
|
+
help: 'Try upgrading your Ghost API client.',
|
|
24
26
|
},
|
|
25
27
|
badVersion: 'Requested version is not supported.',
|
|
26
28
|
actions: {
|
|
27
29
|
images: {
|
|
28
|
-
upload: 'upload image'
|
|
29
|
-
}
|
|
30
|
+
upload: 'upload image',
|
|
31
|
+
},
|
|
30
32
|
},
|
|
31
33
|
userMessages: {
|
|
32
34
|
BookshelfRelationsError: 'Database error, cannot {action}.',
|
|
@@ -50,10 +52,11 @@ const messages = {
|
|
|
50
52
|
EmailError: 'Error sending email!',
|
|
51
53
|
ThemeValidationError: 'Theme validation error, cannot {action}.',
|
|
52
54
|
HostLimitError: 'Host Limit error, cannot {action}.',
|
|
53
|
-
DisabledFeatureError:
|
|
54
|
-
|
|
55
|
+
DisabledFeatureError:
|
|
56
|
+
'Theme validation error, the {{{helperName}}} helper is not available. Cannot {action}.',
|
|
57
|
+
UpdateCollisionError: 'Saving failed! Someone else is editing this post.',
|
|
55
58
|
},
|
|
56
|
-
UnknownError: 'Unknown error - {name}, cannot {action}.'
|
|
59
|
+
UnknownError: 'Unknown error - {name}, cannot {action}.',
|
|
57
60
|
};
|
|
58
61
|
|
|
59
62
|
function isDependencyInStack(dependency, err) {
|
|
@@ -78,18 +81,21 @@ module.exports.prepareError = function prepareError(err, req, res, next) {
|
|
|
78
81
|
// Catch bookshelf empty errors and other 404s, and turn into a Ghost 404
|
|
79
82
|
if ((err.statusCode && err.statusCode === 404) || err.message === 'EmptyResponse') {
|
|
80
83
|
err = new errors.NotFoundError({
|
|
81
|
-
err: err
|
|
84
|
+
err: err,
|
|
82
85
|
});
|
|
83
|
-
|
|
84
|
-
} else if (
|
|
86
|
+
// Catch handlebars / express-hbs errors, and render them as 400, rather than 500 errors as the server isn't broken
|
|
87
|
+
} else if (
|
|
88
|
+
isDependencyInStack('handlebars', err) ||
|
|
89
|
+
isDependencyInStack('express-hbs', err)
|
|
90
|
+
) {
|
|
85
91
|
// Temporary handling of theme errors from handlebars
|
|
86
92
|
// @TODO remove this when #10496 is solved properly
|
|
87
93
|
err = new errors.IncorrectUsageError({
|
|
88
94
|
err: err,
|
|
89
95
|
message: err.message,
|
|
90
|
-
statusCode: err.statusCode
|
|
96
|
+
statusCode: err.statusCode,
|
|
91
97
|
});
|
|
92
|
-
|
|
98
|
+
// Catch database errors and turn them into 500 errors, but log some useful data to sentry
|
|
93
99
|
} else if (isDependencyInStack('mysql2', err)) {
|
|
94
100
|
// we don't want to return raw database errors to our users
|
|
95
101
|
err.sqlErrorCode = err.code;
|
|
@@ -98,24 +104,24 @@ module.exports.prepareError = function prepareError(err, req, res, next) {
|
|
|
98
104
|
err = new errors.ValidationError({
|
|
99
105
|
message: tpl(messages.invalidValue),
|
|
100
106
|
context: err.message,
|
|
101
|
-
err
|
|
107
|
+
err,
|
|
102
108
|
});
|
|
103
109
|
} else {
|
|
104
110
|
err = new errors.InternalServerError({
|
|
105
111
|
err: err,
|
|
106
112
|
message: tpl(messages.genericError),
|
|
107
113
|
statusCode: err.statusCode,
|
|
108
|
-
code: 'UNEXPECTED_ERROR'
|
|
114
|
+
code: 'UNEXPECTED_ERROR',
|
|
109
115
|
});
|
|
110
116
|
}
|
|
111
|
-
|
|
117
|
+
// For everything else, create a generic 500 error, with context set to the original error message
|
|
112
118
|
} else {
|
|
113
119
|
err = new errors.InternalServerError({
|
|
114
120
|
err: err,
|
|
115
121
|
message: tpl(messages.genericError),
|
|
116
122
|
context: err.message,
|
|
117
123
|
statusCode: err.statusCode,
|
|
118
|
-
code: 'UNEXPECTED_ERROR'
|
|
124
|
+
code: 'UNEXPECTED_ERROR',
|
|
119
125
|
});
|
|
120
126
|
}
|
|
121
127
|
}
|
|
@@ -129,7 +135,7 @@ module.exports.prepareError = function prepareError(err, req, res, next) {
|
|
|
129
135
|
next(err);
|
|
130
136
|
};
|
|
131
137
|
|
|
132
|
-
module.exports.prepareStack = function prepareStack(err,
|
|
138
|
+
module.exports.prepareStack = function prepareStack(err, _req, _res, next) {
|
|
133
139
|
const clonedError = prepareStackForUser(err);
|
|
134
140
|
|
|
135
141
|
next(clonedError);
|
|
@@ -142,21 +148,23 @@ module.exports.prepareStack = function prepareStack(err, req, res, next) { // es
|
|
|
142
148
|
* @param {import('express').Response} res
|
|
143
149
|
* @param {import('express').NextFunction} next
|
|
144
150
|
*/
|
|
145
|
-
module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res,
|
|
151
|
+
module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, _next) {
|
|
146
152
|
const userError = prepareUserMessage(err, req);
|
|
147
153
|
|
|
148
154
|
res.json({
|
|
149
|
-
errors: [
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
errors: [
|
|
156
|
+
{
|
|
157
|
+
message: userError.message,
|
|
158
|
+
context: userError.context || null,
|
|
159
|
+
type: err.errorType || null,
|
|
160
|
+
details: err.errorDetails || null,
|
|
161
|
+
property: err.property || null,
|
|
162
|
+
help: err.help || null,
|
|
163
|
+
code: err.code || null,
|
|
164
|
+
id: err.id || null,
|
|
165
|
+
ghostErrorCode: err.ghostErrorCode || null,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
160
168
|
});
|
|
161
169
|
};
|
|
162
170
|
|
|
@@ -164,7 +172,9 @@ module.exports.jsonErrorRenderer = function jsonErrorRenderer(err, req, res, nex
|
|
|
164
172
|
*
|
|
165
173
|
* @param {String} [cacheControlHeaderValue] cache-control header value
|
|
166
174
|
*/
|
|
167
|
-
module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(
|
|
175
|
+
module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(
|
|
176
|
+
cacheControlHeaderValue,
|
|
177
|
+
) {
|
|
168
178
|
return function prepareErrorCacheControlInner(err, req, res, next) {
|
|
169
179
|
let cacheControl = cacheControlHeaderValue;
|
|
170
180
|
if (!cacheControlHeaderValue) {
|
|
@@ -178,7 +188,7 @@ module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cach
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
res.set({
|
|
181
|
-
'Cache-Control': cacheControl
|
|
191
|
+
'Cache-Control': cacheControl,
|
|
182
192
|
});
|
|
183
193
|
|
|
184
194
|
next(err);
|
|
@@ -188,7 +198,7 @@ module.exports.prepareErrorCacheControl = function prepareErrorCacheControl(cach
|
|
|
188
198
|
const prepareUserMessage = function prepareUserMessage(err, req) {
|
|
189
199
|
const userError = {
|
|
190
200
|
message: err.message,
|
|
191
|
-
context: err.context
|
|
201
|
+
context: err.context,
|
|
192
202
|
};
|
|
193
203
|
|
|
194
204
|
const docName = _.get(req, 'frameOptions.docName');
|
|
@@ -202,7 +212,7 @@ const prepareUserMessage = function prepareUserMessage(err, req) {
|
|
|
202
212
|
read: 'read',
|
|
203
213
|
add: 'save',
|
|
204
214
|
edit: 'edit',
|
|
205
|
-
destroy: 'delete'
|
|
215
|
+
destroy: 'delete',
|
|
206
216
|
};
|
|
207
217
|
|
|
208
218
|
if (_.get(messages.actions, [docName, method])) {
|
|
@@ -225,9 +235,9 @@ const prepareUserMessage = function prepareUserMessage(err, req) {
|
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
if (_.get(messages.userMessages, err.name)) {
|
|
228
|
-
userError.message = tpl(messages.userMessages[err.name], {action: action});
|
|
238
|
+
userError.message = tpl(messages.userMessages[err.name], { action: action });
|
|
229
239
|
} else {
|
|
230
|
-
userError.message = tpl(messages.UnknownError, {action, name: err.name});
|
|
240
|
+
userError.message = tpl(messages.UnknownError, { action, name: err.name });
|
|
231
241
|
}
|
|
232
242
|
}
|
|
233
243
|
}
|
|
@@ -240,41 +250,39 @@ module.exports.resourceNotFound = function resourceNotFound(req, res, next) {
|
|
|
240
250
|
// Protect against invalid `Accept-Version` headers
|
|
241
251
|
const acceptVersionSemver = semver.coerce(req.headers['accept-version']);
|
|
242
252
|
if (!acceptVersionSemver) {
|
|
243
|
-
return next(
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
return next(
|
|
254
|
+
new errors.BadRequestError({
|
|
255
|
+
message: tpl(messages.badVersion),
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
if (semver.compare(acceptVersionSemver, semver.coerce(res.locals.safeVersion)) !== 0) {
|
|
249
261
|
const versionComparison = semver.compare(
|
|
250
262
|
acceptVersionSemver,
|
|
251
|
-
semver.coerce(res.locals.safeVersion)
|
|
263
|
+
semver.coerce(res.locals.safeVersion),
|
|
252
264
|
);
|
|
253
265
|
|
|
254
266
|
let notAcceptableError;
|
|
255
267
|
if (versionComparison === 1) {
|
|
256
268
|
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
257
|
-
message: tpl(
|
|
258
|
-
messages.methodNotAcceptableVersionAhead.message
|
|
259
|
-
),
|
|
269
|
+
message: tpl(messages.methodNotAcceptableVersionAhead.message),
|
|
260
270
|
context: tpl(messages.methodNotAcceptableVersionAhead.context, {
|
|
261
271
|
acceptVersion: req.headers['accept-version'],
|
|
262
|
-
ghostVersion: `v${res.locals.safeVersion}
|
|
272
|
+
ghostVersion: `v${res.locals.safeVersion}`,
|
|
263
273
|
}),
|
|
264
274
|
help: tpl(messages.methodNotAcceptableVersionAhead.help),
|
|
265
|
-
code: 'UPDATE_GHOST'
|
|
275
|
+
code: 'UPDATE_GHOST',
|
|
266
276
|
});
|
|
267
277
|
} else {
|
|
268
278
|
notAcceptableError = new errors.RequestNotAcceptableError({
|
|
269
|
-
message: tpl(
|
|
270
|
-
messages.methodNotAcceptableVersionBehind.message
|
|
271
|
-
),
|
|
279
|
+
message: tpl(messages.methodNotAcceptableVersionBehind.message),
|
|
272
280
|
context: tpl(messages.methodNotAcceptableVersionBehind.context, {
|
|
273
281
|
acceptVersion: req.headers['accept-version'],
|
|
274
|
-
ghostVersion: `v${res.locals.safeVersion}
|
|
282
|
+
ghostVersion: `v${res.locals.safeVersion}`,
|
|
275
283
|
}),
|
|
276
284
|
help: tpl(messages.methodNotAcceptableVersionBehind.help),
|
|
277
|
-
code: 'UPDATE_CLIENT'
|
|
285
|
+
code: 'UPDATE_CLIENT',
|
|
278
286
|
});
|
|
279
287
|
}
|
|
280
288
|
|
|
@@ -282,14 +290,14 @@ module.exports.resourceNotFound = function resourceNotFound(req, res, next) {
|
|
|
282
290
|
}
|
|
283
291
|
}
|
|
284
292
|
|
|
285
|
-
next(new errors.NotFoundError({message: tpl(messages.resourceNotFound)}));
|
|
293
|
+
next(new errors.NotFoundError({ message: tpl(messages.resourceNotFound) }));
|
|
286
294
|
};
|
|
287
295
|
|
|
288
296
|
module.exports.pageNotFound = function pageNotFound(req, res, next) {
|
|
289
|
-
next(new errors.NotFoundError({message: tpl(messages.pageNotFound)}));
|
|
297
|
+
next(new errors.NotFoundError({ message: tpl(messages.pageNotFound) }));
|
|
290
298
|
};
|
|
291
299
|
|
|
292
|
-
module.exports.handleJSONResponse = sentry => [
|
|
300
|
+
module.exports.handleJSONResponse = (sentry) => [
|
|
293
301
|
// Make sure the error can be served
|
|
294
302
|
module.exports.prepareError,
|
|
295
303
|
// Add cache-control header
|
|
@@ -299,10 +307,10 @@ module.exports.handleJSONResponse = sentry => [
|
|
|
299
307
|
// Format the stack for the user
|
|
300
308
|
module.exports.prepareStack,
|
|
301
309
|
// Render the error using JSON format
|
|
302
|
-
module.exports.jsonErrorRenderer
|
|
310
|
+
module.exports.jsonErrorRenderer,
|
|
303
311
|
];
|
|
304
312
|
|
|
305
|
-
module.exports.handleHTMLResponse = sentry => [
|
|
313
|
+
module.exports.handleHTMLResponse = (sentry) => [
|
|
306
314
|
// Make sure the error can be served
|
|
307
315
|
module.exports.prepareError,
|
|
308
316
|
// Add cache-control header
|
|
@@ -310,5 +318,5 @@ module.exports.handleHTMLResponse = sentry => [
|
|
|
310
318
|
// Handle the error in Sentry
|
|
311
319
|
sentry.errorHandler,
|
|
312
320
|
// Format the stack for the user
|
|
313
|
-
module.exports.prepareStack
|
|
321
|
+
module.exports.prepareStack,
|
|
314
322
|
];
|
package/package.json
CHANGED
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/mw-error-handler",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Express middleware utilities for normalizing and rendering Ghost API errors",
|
|
5
|
-
"author": "Ghost Foundation",
|
|
6
5
|
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"dev": "echo \"Implement me!\"",
|
|
13
|
-
"test:unit": "NODE_ENV=testing vitest run --coverage --config ../../vitest.config.ts",
|
|
14
|
-
"test": "yarn test:unit",
|
|
15
|
-
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
|
16
|
-
"lint": "yarn lint:code && yarn lint:test",
|
|
17
|
-
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
|
6
|
+
"author": "Ghost Foundation",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TryGhost/framework.git",
|
|
10
|
+
"directory": "packages/mw-error-handler"
|
|
18
11
|
},
|
|
19
12
|
"files": [
|
|
20
13
|
"index.js",
|
|
21
14
|
"lib"
|
|
22
15
|
],
|
|
23
|
-
"
|
|
24
|
-
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
25
19
|
},
|
|
26
20
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"@tryghost/
|
|
30
|
-
"@tryghost/
|
|
31
|
-
"
|
|
32
|
-
"
|
|
21
|
+
"lodash": "4.18.1",
|
|
22
|
+
"semver": "7.7.4",
|
|
23
|
+
"@tryghost/debug": "2.2.0",
|
|
24
|
+
"@tryghost/errors": "3.2.0",
|
|
25
|
+
"@tryghost/http-cache-utils": "2.2.0",
|
|
26
|
+
"@tryghost/tpl": "2.2.0"
|
|
33
27
|
},
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"sinon": "21.1.2"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "echo \"Implement me!\"",
|
|
33
|
+
"test": "NODE_ENV=testing vitest run --coverage",
|
|
34
|
+
"lint": "oxlint -c ../../.oxlintrc.json ."
|
|
38
35
|
}
|
|
39
|
-
}
|
|
36
|
+
}
|