@tryghost/errors 0.2.15 → 1.0.1
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/lib/errors.js +208 -16
- package/lib/utils.js +200 -0
- package/package.json +6 -7
package/lib/errors.js
CHANGED
|
@@ -1,17 +1,203 @@
|
|
|
1
|
+
const uuid = require('uuid');
|
|
1
2
|
const merge = require('lodash/merge');
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const errors = require('@tryghost/ignition-errors');
|
|
3
|
+
const isString = require('lodash/isString');
|
|
4
|
+
const utils = require('./utils');
|
|
5
5
|
|
|
6
|
-
class GhostError extends
|
|
6
|
+
class GhostError extends Error {
|
|
7
7
|
constructor(options) {
|
|
8
8
|
options = options || {};
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* defaults
|
|
14
|
+
*/
|
|
15
|
+
this.statusCode = 500;
|
|
16
|
+
this.errorType = 'InternalServerError';
|
|
17
|
+
this.level = 'normal';
|
|
18
|
+
this.message = 'The server has encountered an error.';
|
|
19
|
+
this.id = uuid.v1();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* custom overrides
|
|
23
|
+
*/
|
|
24
|
+
this.id = options.id || this.id;
|
|
25
|
+
this.statusCode = options.statusCode || this.statusCode;
|
|
26
|
+
this.level = options.level || this.level;
|
|
27
|
+
this.context = options.context || this.context;
|
|
28
|
+
this.help = options.help || this.help;
|
|
29
|
+
this.errorType = this.name = options.errorType || this.errorType;
|
|
30
|
+
this.errorDetails = options.errorDetails;
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
this.code = options.code || null;
|
|
33
|
+
this.property = options.property || null;
|
|
34
|
+
this.redirect = options.redirect || null;
|
|
35
|
+
|
|
36
|
+
this.message = options.message || this.message;
|
|
37
|
+
this.hideStack = options.hideStack;
|
|
38
|
+
|
|
39
|
+
// NOTE: Error to inherit from, override!
|
|
40
|
+
// Nested objects are getting copied over in one piece (can be changed, but not needed right now)
|
|
41
|
+
if (options.err) {
|
|
42
|
+
// CASE: Support err as string (it happens that third party libs return a string instead of an error instance)
|
|
43
|
+
if (isString(options.err)) {
|
|
44
|
+
/* eslint-disable no-restricted-syntax */
|
|
45
|
+
options.err = new Error(options.err);
|
|
46
|
+
/* eslint-enable no-restricted-syntax */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Object.getOwnPropertyNames(options.err).forEach((property) => {
|
|
50
|
+
if (['errorType', 'name', 'statusCode', 'message', 'level'].indexOf(property) !== -1) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// CASE: `code` should put options as priority over err
|
|
55
|
+
if (property === 'code') {
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
this[property] = this[property] || options.err[property];
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (property === 'stack') {
|
|
62
|
+
this[property] += '\n\n' + options.err[property];
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this[property] = options.err[property] || this[property];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
11
69
|
}
|
|
12
70
|
}
|
|
13
71
|
|
|
14
72
|
const ghostErrors = {
|
|
73
|
+
InternalServerError: class InternalServerError extends GhostError {
|
|
74
|
+
constructor(options) {
|
|
75
|
+
super(merge({
|
|
76
|
+
statusCode: 500,
|
|
77
|
+
level: 'critical',
|
|
78
|
+
errorType: 'InternalServerError',
|
|
79
|
+
message: 'The server has encountered an error.'
|
|
80
|
+
}, options));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
IncorrectUsageError: class IncorrectUsageError extends GhostError {
|
|
84
|
+
constructor(options) {
|
|
85
|
+
super(merge({
|
|
86
|
+
statusCode: 400,
|
|
87
|
+
level: 'critical',
|
|
88
|
+
errorType: 'IncorrectUsageError',
|
|
89
|
+
message: 'We detected a misuse. Please read the stack trace.'
|
|
90
|
+
}, options));
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
NotFoundError: class NotFoundError extends GhostError {
|
|
94
|
+
constructor(options) {
|
|
95
|
+
super(merge({
|
|
96
|
+
statusCode: 404,
|
|
97
|
+
errorType: 'NotFoundError',
|
|
98
|
+
message: 'Resource could not be found.'
|
|
99
|
+
}, options));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
BadRequestError: class BadRequestError extends GhostError {
|
|
103
|
+
constructor(options) {
|
|
104
|
+
super(merge({
|
|
105
|
+
statusCode: 400,
|
|
106
|
+
errorType: 'BadRequestError',
|
|
107
|
+
message: 'The request could not be understood.'
|
|
108
|
+
}, options));
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
UnauthorizedError: class UnauthorizedError extends GhostError {
|
|
112
|
+
constructor(options) {
|
|
113
|
+
super(merge({
|
|
114
|
+
statusCode: 401,
|
|
115
|
+
errorType: 'UnauthorizedError',
|
|
116
|
+
message: 'You are not authorised to make this request.'
|
|
117
|
+
}, options));
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
NoPermissionError: class NoPermissionError extends GhostError {
|
|
121
|
+
constructor(options) {
|
|
122
|
+
super(merge({
|
|
123
|
+
statusCode: 403,
|
|
124
|
+
errorType: 'NoPermissionError',
|
|
125
|
+
message: 'You do not have permission to perform this request.'
|
|
126
|
+
}, options));
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
ValidationError: class ValidationError extends GhostError {
|
|
130
|
+
constructor(options) {
|
|
131
|
+
super(merge({
|
|
132
|
+
statusCode: 422,
|
|
133
|
+
errorType: 'ValidationError',
|
|
134
|
+
message: 'The request failed validation.'
|
|
135
|
+
}, options));
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
UnsupportedMediaTypeError: class UnsupportedMediaTypeError extends GhostError {
|
|
139
|
+
constructor(options) {
|
|
140
|
+
super(merge({
|
|
141
|
+
statusCode: 415,
|
|
142
|
+
errorType: 'UnsupportedMediaTypeError',
|
|
143
|
+
message: 'The media in the request is not supported by the server.'
|
|
144
|
+
}, options));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
TooManyRequestsError: class TooManyRequestsError extends GhostError {
|
|
148
|
+
constructor(options) {
|
|
149
|
+
super(merge({
|
|
150
|
+
statusCode: 429,
|
|
151
|
+
errorType: 'TooManyRequestsError',
|
|
152
|
+
message: 'Server has received too many similar requests in a short space of time.'
|
|
153
|
+
}, options));
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
MaintenanceError: class MaintenanceError extends GhostError {
|
|
157
|
+
constructor(options) {
|
|
158
|
+
super(merge({
|
|
159
|
+
statusCode: 503,
|
|
160
|
+
errorType: 'MaintenanceError',
|
|
161
|
+
message: 'The server is temporarily down for maintenance.'
|
|
162
|
+
}, options));
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
MethodNotAllowedError: class MethodNotAllowedError extends GhostError {
|
|
166
|
+
constructor(options) {
|
|
167
|
+
super(merge({
|
|
168
|
+
statusCode: 405,
|
|
169
|
+
errorType: 'MethodNotAllowedError',
|
|
170
|
+
message: 'Method not allowed for resource.'
|
|
171
|
+
}, options));
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
RequestEntityTooLargeError: class RequestEntityTooLargeError extends GhostError {
|
|
175
|
+
constructor(options) {
|
|
176
|
+
super(merge({
|
|
177
|
+
statusCode: 413,
|
|
178
|
+
errorType: 'RequestEntityTooLargeError',
|
|
179
|
+
message: 'Request was too big for the server to handle.'
|
|
180
|
+
}, options));
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
TokenRevocationError: class TokenRevocationError extends GhostError {
|
|
184
|
+
constructor(options) {
|
|
185
|
+
super(merge({
|
|
186
|
+
statusCode: 503,
|
|
187
|
+
errorType: 'TokenRevocationError',
|
|
188
|
+
message: 'Token is no longer available.'
|
|
189
|
+
}, options));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
VersionMismatchError: class VersionMismatchError extends GhostError {
|
|
193
|
+
constructor(options) {
|
|
194
|
+
super(merge({
|
|
195
|
+
statusCode: 400,
|
|
196
|
+
errorType: 'VersionMismatchError',
|
|
197
|
+
message: 'Requested version does not match server version.'
|
|
198
|
+
}, options));
|
|
199
|
+
}
|
|
200
|
+
},
|
|
15
201
|
DataExportError: class DataExportError extends GhostError {
|
|
16
202
|
constructor(options) {
|
|
17
203
|
super(merge({
|
|
@@ -95,17 +281,23 @@ const ghostErrors = {
|
|
|
95
281
|
message: 'For security, you need to create a new password. An email has been sent to you with instructions!'
|
|
96
282
|
}, options));
|
|
97
283
|
}
|
|
284
|
+
},
|
|
285
|
+
UnhandledJobError: class UnhandledJobError extends GhostError {
|
|
286
|
+
constructor(options) {
|
|
287
|
+
super(merge({
|
|
288
|
+
errorType: 'UnhandledJobError',
|
|
289
|
+
message: 'Processed job threw an unhandled error',
|
|
290
|
+
level: 'critical'
|
|
291
|
+
}, options));
|
|
292
|
+
}
|
|
98
293
|
}
|
|
99
294
|
};
|
|
100
295
|
|
|
101
|
-
|
|
102
|
-
each(errors, function (error) {
|
|
103
|
-
if (error.name === 'IgnitionError' || typeof error === 'object') {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
296
|
+
module.exports = ghostErrors;
|
|
106
297
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
298
|
+
const ghostErrorsWithBase = Object.assign({}, ghostErrors, {GhostError});
|
|
299
|
+
module.exports.utils = {
|
|
300
|
+
serialize: utils.serialize.bind(ghostErrorsWithBase),
|
|
301
|
+
deserialize: utils.deserialize.bind(ghostErrorsWithBase),
|
|
302
|
+
isGhostError: utils.isGhostError.bind(ghostErrorsWithBase)
|
|
303
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const omit = require('lodash/omit');
|
|
2
|
+
const merge = require('lodash/merge');
|
|
3
|
+
const extend = require('lodash/extend');
|
|
4
|
+
const _private = {};
|
|
5
|
+
|
|
6
|
+
_private.serialize = function serialize(err) {
|
|
7
|
+
try {
|
|
8
|
+
return {
|
|
9
|
+
id: err.id,
|
|
10
|
+
status: err.statusCode,
|
|
11
|
+
code: err.code || err.errorType,
|
|
12
|
+
title: err.name,
|
|
13
|
+
detail: err.message,
|
|
14
|
+
meta: {
|
|
15
|
+
context: err.context,
|
|
16
|
+
help: err.help,
|
|
17
|
+
errorDetails: err.errorDetails,
|
|
18
|
+
level: err.level,
|
|
19
|
+
errorType: err.errorType
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return {
|
|
24
|
+
detail: 'Something went wrong.'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
_private.deserialize = function deserialize(obj) {
|
|
30
|
+
try {
|
|
31
|
+
return {
|
|
32
|
+
id: obj.id,
|
|
33
|
+
message: obj.detail || obj.error_description || obj.message,
|
|
34
|
+
statusCode: obj.status,
|
|
35
|
+
code: obj.code || obj.error,
|
|
36
|
+
level: obj.meta && obj.meta.level,
|
|
37
|
+
help: obj.meta && obj.meta.help,
|
|
38
|
+
context: obj.meta && obj.meta.context
|
|
39
|
+
};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
message: 'Something went wrong.'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @description Serialize error instance into oauth format.
|
|
49
|
+
*
|
|
50
|
+
* @see https://tools.ietf.org/html/rfc6749#page-45
|
|
51
|
+
*
|
|
52
|
+
* To not loose any error data when sending errors between internal services, we use the suggested OAuth properties and add ours as well.
|
|
53
|
+
*/
|
|
54
|
+
_private.OAuthSerialize = function OAuthSerialize(err) {
|
|
55
|
+
const matchTable = {};
|
|
56
|
+
|
|
57
|
+
matchTable[this.NoPermissionError.name] = 'access_denied';
|
|
58
|
+
matchTable[this.MaintenanceError.name] = 'temporarily_unavailable';
|
|
59
|
+
matchTable[this.BadRequestError.name] = matchTable[this.ValidationError.name] = 'invalid_request';
|
|
60
|
+
matchTable.default = 'server_error';
|
|
61
|
+
|
|
62
|
+
return merge({
|
|
63
|
+
error: err.code || matchTable[err.name] || 'server_error',
|
|
64
|
+
error_description: err.message
|
|
65
|
+
}, omit(_private.serialize(err), ['detail', 'code']));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @description Deserialize oauth error format into GhostError instance.
|
|
70
|
+
* @param {Object} errorFormat
|
|
71
|
+
* @return {Error}
|
|
72
|
+
* @constructor
|
|
73
|
+
*/
|
|
74
|
+
_private.OAuthDeserialize = function OAuthDeserialize(errorFormat) {
|
|
75
|
+
try {
|
|
76
|
+
return new this[errorFormat.title || errorFormat.name || this.InternalServerError.name](_private.deserialize(errorFormat));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// CASE: you receive an OAuth formatted error, but the error prototype is unknown
|
|
79
|
+
return new this.InternalServerError(extend({
|
|
80
|
+
errorType: errorFormat.title || errorFormat.name
|
|
81
|
+
}, _private.deserialize(errorFormat)));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @description Serialize GhostError instance into jsonapi.org format.
|
|
87
|
+
* @param {Error} err
|
|
88
|
+
* @return {Object}
|
|
89
|
+
*/
|
|
90
|
+
_private.JSONAPISerialize = function JSONAPISerialize(err) {
|
|
91
|
+
const errorFormat = {
|
|
92
|
+
errors: [_private.serialize(err)]
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
errorFormat.errors[0].source = {};
|
|
96
|
+
|
|
97
|
+
if (err.property) {
|
|
98
|
+
errorFormat.errors[0].source.pointer = '/data/attributes/' + err.property;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return errorFormat;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @description Deserialize JSON api format into GhostError instance.
|
|
106
|
+
* @param {Object} errorFormat
|
|
107
|
+
* @return {Error}
|
|
108
|
+
*/
|
|
109
|
+
_private.JSONAPIDeserialize = function JSONAPIDeserialize(errorFormat) {
|
|
110
|
+
errorFormat = errorFormat.errors && errorFormat.errors[0] || {};
|
|
111
|
+
|
|
112
|
+
let internalError;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
internalError = new this[errorFormat.title || errorFormat.name || this.InternalServerError.name](_private.deserialize(errorFormat));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// CASE: you receive a JSON format error, but the error prototype is unknown
|
|
118
|
+
internalError = new this.InternalServerError(extend({
|
|
119
|
+
errorType: errorFormat.title || errorFormat.name
|
|
120
|
+
}, _private.deserialize(errorFormat)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (errorFormat.source && errorFormat.source.pointer) {
|
|
124
|
+
internalError.property = errorFormat.source.pointer.split('/')[3];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return internalError;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @description Serialize GhostError instance to error JSON format
|
|
132
|
+
*
|
|
133
|
+
* jsonapi.org error format:
|
|
134
|
+
*
|
|
135
|
+
* source: {
|
|
136
|
+
* parameter: URL query parameter (no support yet)
|
|
137
|
+
* pointer: HTTP body attribute
|
|
138
|
+
* }
|
|
139
|
+
*
|
|
140
|
+
* @see http://jsonapi.org/format/#errors
|
|
141
|
+
*
|
|
142
|
+
* @param {Error} err
|
|
143
|
+
* @param {Object} options { format: [String] (jsonapi || oauth) }
|
|
144
|
+
*/
|
|
145
|
+
exports.serialize = function serialize(err, options) {
|
|
146
|
+
options = options || {format: 'jsonapi'};
|
|
147
|
+
|
|
148
|
+
let errorFormat = {};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (options.format === 'jsonapi') {
|
|
152
|
+
errorFormat = _private.JSONAPISerialize.bind(this)(err);
|
|
153
|
+
} else {
|
|
154
|
+
errorFormat = _private.OAuthSerialize.bind(this)(err);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
errorFormat.message = 'Something went wrong.';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// no need to sanitize the undefined values, on response send JSON.stringify get's called
|
|
161
|
+
return errorFormat;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @description Deserialize from error JSON format to GhostError instance
|
|
166
|
+
* @param {Object} errorFormat
|
|
167
|
+
*/
|
|
168
|
+
exports.deserialize = function deserialize(errorFormat) {
|
|
169
|
+
let internalError = {};
|
|
170
|
+
|
|
171
|
+
if (errorFormat.errors) {
|
|
172
|
+
internalError = _private.JSONAPIDeserialize.bind(this)(errorFormat);
|
|
173
|
+
} else {
|
|
174
|
+
internalError = _private.OAuthDeserialize.bind(this)(errorFormat);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return internalError;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @description Check whether an error instance is a GhostError.
|
|
182
|
+
*/
|
|
183
|
+
exports.isGhostError = function isGhostError(err) {
|
|
184
|
+
const errorName = this.GhostError.name;
|
|
185
|
+
|
|
186
|
+
const recursiveIsGhostError = function recursiveIsGhostError(obj) {
|
|
187
|
+
// no super constructor available anymore
|
|
188
|
+
if (!obj || !obj.name) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (obj.name === errorName) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return recursiveIsGhostError(Object.getPrototypeOf(obj));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return recursiveIsGhostError(err.constructor);
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryghost/errors",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"repository": "https://github.com/TryGhost/Utils/tree/
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"repository": "https://github.com/TryGhost/Utils/tree/main/packages/errors",
|
|
5
5
|
"author": "Ghost Foundation",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"dev": "echo \"Implement me!\"",
|
|
10
|
-
"test": "NODE_ENV=testing c8 mocha './test/**/*.test.js'",
|
|
10
|
+
"test": "NODE_ENV=testing c8 --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
|
11
11
|
"lint": "eslint . --ext .js --cache",
|
|
12
12
|
"posttest": "yarn lint"
|
|
13
13
|
},
|
|
@@ -19,14 +19,13 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"c8": "7.
|
|
23
|
-
"mocha": "9.1.
|
|
22
|
+
"c8": "7.10.0",
|
|
23
|
+
"mocha": "9.1.3",
|
|
24
24
|
"should": "13.2.3",
|
|
25
25
|
"sinon": "11.1.2"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@tryghost/ignition-errors": "^0.1.0",
|
|
29
28
|
"lodash": "^4.17.21"
|
|
30
29
|
},
|
|
31
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "cef9c2f09117b0966380653a9c2be7aa409a9f86"
|
|
32
31
|
}
|