@tryghost/errors 1.2.26 → 1.3.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/cjs/GhostError.js +83 -0
- package/cjs/errors.js +319 -0
- package/cjs/index.js +45 -0
- package/cjs/utils.js +204 -0
- package/cjs/wrap-stack.js +29 -0
- package/es/GhostError.js +64 -0
- package/es/errors.js +299 -0
- package/es/index.js +14 -0
- package/es/utils.js +174 -0
- package/es/wrap-stack.js +9 -0
- package/package.json +26 -9
- package/src/GhostError.ts +96 -0
- package/src/errors.ts +301 -0
- package/src/index.ts +14 -0
- package/src/utils.ts +238 -0
- package/src/wrap-stack.ts +5 -0
- package/types/GhostError.d.ts +30 -0
- package/types/GhostError.d.ts.map +1 -0
- package/types/errors.d.ts +86 -0
- package/types/errors.d.ts.map +1 -0
- package/types/index.d.ts +13 -0
- package/types/index.d.ts.map +1 -0
- package/types/utils.d.ts +33 -0
- package/types/utils.d.ts.map +1 -0
- package/types/wrap-stack.d.ts +2 -0
- package/types/wrap-stack.d.ts.map +1 -0
- package/index.js +0 -1
- package/lib/errors.js +0 -340
- package/lib/utils.js +0 -237
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {v1 as uuidv1} from 'uuid';
|
|
2
|
+
import {wrapStack} from './wrap-stack';
|
|
3
|
+
|
|
4
|
+
export interface GhostErrorOptions {
|
|
5
|
+
message?: string;
|
|
6
|
+
statusCode?: number;
|
|
7
|
+
level?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
context?: string;
|
|
10
|
+
help?: string;
|
|
11
|
+
errorType?: string;
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
errorDetails?: any;
|
|
14
|
+
code?: string;
|
|
15
|
+
property?: string;
|
|
16
|
+
redirect?: string;
|
|
17
|
+
hideStack?: boolean;
|
|
18
|
+
err?: Error | string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class GhostError extends Error {
|
|
22
|
+
statusCode: number;
|
|
23
|
+
errorType: string;
|
|
24
|
+
level: string;
|
|
25
|
+
id: string;
|
|
26
|
+
context?: string;
|
|
27
|
+
help?: string;
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
errorDetails: any;
|
|
30
|
+
code: string | null;
|
|
31
|
+
property: string | null;
|
|
32
|
+
redirect: string | null;
|
|
33
|
+
hideStack: boolean;
|
|
34
|
+
|
|
35
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
36
|
+
super();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* defaults
|
|
40
|
+
*/
|
|
41
|
+
this.statusCode = 500;
|
|
42
|
+
this.errorType = 'InternalServerError';
|
|
43
|
+
this.level = 'normal';
|
|
44
|
+
this.message = 'The server has encountered an error.';
|
|
45
|
+
this.id = uuidv1();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* custom overrides
|
|
49
|
+
*/
|
|
50
|
+
this.id = options.id || this.id;
|
|
51
|
+
this.statusCode = options.statusCode || this.statusCode;
|
|
52
|
+
this.level = options.level || this.level;
|
|
53
|
+
this.context = options.context;
|
|
54
|
+
this.help = options.help;
|
|
55
|
+
this.errorType = this.name = options.errorType || this.errorType;
|
|
56
|
+
this.errorDetails = options.errorDetails;
|
|
57
|
+
this.code = options.code || null;
|
|
58
|
+
this.property = options.property || null;
|
|
59
|
+
this.redirect = options.redirect || null;
|
|
60
|
+
|
|
61
|
+
this.message = options.message || this.message;
|
|
62
|
+
this.hideStack = options.hideStack || false;
|
|
63
|
+
|
|
64
|
+
// NOTE: Error to inherit from, override!
|
|
65
|
+
// Nested objects are getting copied over in one piece (can be changed, but not needed right now)
|
|
66
|
+
if (options.err) {
|
|
67
|
+
// CASE: Support err as string (it happens that third party libs return a string instead of an error instance)
|
|
68
|
+
if (typeof options.err === 'string') {
|
|
69
|
+
/* eslint-disable no-restricted-syntax */
|
|
70
|
+
options.err = new Error(options.err);
|
|
71
|
+
/* eslint-enable no-restricted-syntax */
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Object.getOwnPropertyNames(options.err).forEach((property) => {
|
|
75
|
+
if (['errorType', 'name', 'statusCode', 'message', 'level'].indexOf(property) !== -1) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// CASE: `code` should put options as priority over err
|
|
80
|
+
if (property === 'code') {
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
this[property] = this[property] || (options.err as any)[property];
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (property === 'stack' && !this.hideStack) {
|
|
87
|
+
this[property] = wrapStack(this, options.err as Error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
(this as any)[property] = (options.err as any)[property] || (this as any)[property];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {GhostError, GhostErrorOptions} from './GhostError';
|
|
2
|
+
|
|
3
|
+
const mergeOptions = (options: GhostErrorOptions, defaults: GhostErrorOptions) => {
|
|
4
|
+
const result = {...defaults};
|
|
5
|
+
|
|
6
|
+
// Ignore undefined options - for example passing statusCode: undefined should not override the default
|
|
7
|
+
(Object.keys(options) as (keyof GhostErrorOptions)[]).forEach((key) => {
|
|
8
|
+
if (options[key] !== undefined) {
|
|
9
|
+
result[key] = options[key];
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class InternalServerError extends GhostError {
|
|
17
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
18
|
+
super(mergeOptions(options, {
|
|
19
|
+
statusCode: 500,
|
|
20
|
+
level: 'critical',
|
|
21
|
+
errorType: 'InternalServerError',
|
|
22
|
+
message: 'The server has encountered an error.'
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class IncorrectUsageError extends GhostError {
|
|
28
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
29
|
+
super(mergeOptions(options, {
|
|
30
|
+
statusCode: 400,
|
|
31
|
+
level: 'critical',
|
|
32
|
+
errorType: 'IncorrectUsageError',
|
|
33
|
+
message: 'We detected a misuse. Please read the stack trace.'
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class NotFoundError extends GhostError {
|
|
39
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
40
|
+
super(mergeOptions(options, {
|
|
41
|
+
statusCode: 404,
|
|
42
|
+
errorType: 'NotFoundError',
|
|
43
|
+
message: 'Resource could not be found.',
|
|
44
|
+
hideStack: true
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class BadRequestError extends GhostError {
|
|
50
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
51
|
+
super(mergeOptions(options, {
|
|
52
|
+
statusCode: 400,
|
|
53
|
+
errorType: 'BadRequestError',
|
|
54
|
+
message: 'The request could not be understood.'
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class UnauthorizedError extends GhostError {
|
|
60
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
61
|
+
super(mergeOptions(options, {
|
|
62
|
+
statusCode: 401,
|
|
63
|
+
errorType: 'UnauthorizedError',
|
|
64
|
+
message: 'You are not authorised to make this request.'
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class NoPermissionError extends GhostError {
|
|
70
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
71
|
+
super(mergeOptions(options, {
|
|
72
|
+
statusCode: 403,
|
|
73
|
+
errorType: 'NoPermissionError',
|
|
74
|
+
message: 'You do not have permission to perform this request.'
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class ValidationError extends GhostError {
|
|
80
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
81
|
+
super(mergeOptions(options, {
|
|
82
|
+
statusCode: 422,
|
|
83
|
+
errorType: 'ValidationError',
|
|
84
|
+
message: 'The request failed validation.'
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class UnsupportedMediaTypeError extends GhostError {
|
|
90
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
91
|
+
super(mergeOptions(options, {
|
|
92
|
+
statusCode: 415,
|
|
93
|
+
errorType: 'UnsupportedMediaTypeError',
|
|
94
|
+
message: 'The media in the request is not supported by the server.'
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class TooManyRequestsError extends GhostError {
|
|
100
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
101
|
+
super(mergeOptions(options, {
|
|
102
|
+
statusCode: 429,
|
|
103
|
+
errorType: 'TooManyRequestsError',
|
|
104
|
+
message: 'Server has received too many similar requests in a short space of time.'
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class MaintenanceError extends GhostError {
|
|
110
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
111
|
+
super(mergeOptions(options, {
|
|
112
|
+
statusCode: 503,
|
|
113
|
+
errorType: 'MaintenanceError',
|
|
114
|
+
message: 'The server is temporarily down for maintenance.'
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class MethodNotAllowedError extends GhostError {
|
|
120
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
121
|
+
super(mergeOptions(options, {
|
|
122
|
+
statusCode: 405,
|
|
123
|
+
errorType: 'MethodNotAllowedError',
|
|
124
|
+
message: 'Method not allowed for resource.'
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class RequestNotAcceptableError extends GhostError {
|
|
130
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
131
|
+
super(mergeOptions(options, {
|
|
132
|
+
statusCode: 406,
|
|
133
|
+
errorType: 'RequestNotAcceptableError',
|
|
134
|
+
message: 'Request not acceptable for provided Accept-Version header.',
|
|
135
|
+
hideStack: true
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class RequestEntityTooLargeError extends GhostError {
|
|
141
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
142
|
+
super(mergeOptions(options, {
|
|
143
|
+
statusCode: 413,
|
|
144
|
+
errorType: 'RequestEntityTooLargeError',
|
|
145
|
+
message: 'Request was too big for the server to handle.'
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class TokenRevocationError extends GhostError {
|
|
151
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
152
|
+
super(mergeOptions(options, {
|
|
153
|
+
statusCode: 503,
|
|
154
|
+
errorType: 'TokenRevocationError',
|
|
155
|
+
message: 'Token is no longer available.'
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class VersionMismatchError extends GhostError {
|
|
161
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
162
|
+
super(mergeOptions(options, {
|
|
163
|
+
statusCode: 400,
|
|
164
|
+
errorType: 'VersionMismatchError',
|
|
165
|
+
message: 'Requested version does not match server version.'
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class DataExportError extends GhostError {
|
|
171
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
172
|
+
super(mergeOptions(options, {
|
|
173
|
+
statusCode: 500,
|
|
174
|
+
errorType: 'DataExportError',
|
|
175
|
+
message: 'The server encountered an error whilst exporting data.'
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class DataImportError extends GhostError {
|
|
181
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
182
|
+
super(mergeOptions(options, {
|
|
183
|
+
statusCode: 500,
|
|
184
|
+
errorType: 'DataImportError',
|
|
185
|
+
message: 'The server encountered an error whilst importing data.'
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class EmailError extends GhostError {
|
|
191
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
192
|
+
super(mergeOptions(options, {
|
|
193
|
+
statusCode: 500,
|
|
194
|
+
errorType: 'EmailError',
|
|
195
|
+
message: 'The server encountered an error whilst sending email.'
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class ThemeValidationError extends GhostError {
|
|
201
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
202
|
+
super(mergeOptions(options, {
|
|
203
|
+
statusCode: 422,
|
|
204
|
+
errorType: 'ThemeValidationError',
|
|
205
|
+
message: 'The theme has a validation error.',
|
|
206
|
+
errorDetails: {}
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export class DisabledFeatureError extends GhostError {
|
|
212
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
213
|
+
super(mergeOptions(options, {
|
|
214
|
+
statusCode: 409,
|
|
215
|
+
errorType: 'DisabledFeatureError',
|
|
216
|
+
message: 'Unable to complete the request, this feature is disabled.'
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export class UpdateCollisionError extends GhostError {
|
|
222
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
223
|
+
super(mergeOptions(options, {
|
|
224
|
+
statusCode: 409,
|
|
225
|
+
errorType: 'UpdateCollisionError',
|
|
226
|
+
message: 'Unable to complete the request, there was a conflict.'
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export class HostLimitError extends GhostError {
|
|
232
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
233
|
+
super(mergeOptions(options, {
|
|
234
|
+
errorType: 'HostLimitError',
|
|
235
|
+
hideStack: true,
|
|
236
|
+
statusCode: 403,
|
|
237
|
+
message: 'Unable to complete the request, this resource is limited.'
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export class HelperWarning extends GhostError {
|
|
243
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
244
|
+
super(mergeOptions(options, {
|
|
245
|
+
errorType: 'HelperWarning',
|
|
246
|
+
hideStack: true,
|
|
247
|
+
statusCode: 400,
|
|
248
|
+
message: 'A theme helper has done something unexpected.'
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export class PasswordResetRequiredError extends GhostError {
|
|
254
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
255
|
+
super(mergeOptions(options, {
|
|
256
|
+
errorType: 'PasswordResetRequiredError',
|
|
257
|
+
statusCode: 401,
|
|
258
|
+
message: 'For security, you need to create a new password. An email has been sent to you with instructions!'
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class UnhandledJobError extends GhostError {
|
|
264
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
265
|
+
super(mergeOptions(options, {
|
|
266
|
+
errorType: 'UnhandledJobError',
|
|
267
|
+
message: 'Processed job threw an unhandled error',
|
|
268
|
+
level: 'critical'
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class NoContentError extends GhostError {
|
|
274
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
275
|
+
super(mergeOptions(options, {
|
|
276
|
+
errorType: 'NoContentError',
|
|
277
|
+
statusCode: 204,
|
|
278
|
+
hideStack: true
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export class ConflictError extends GhostError {
|
|
284
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
285
|
+
super(mergeOptions(options, {
|
|
286
|
+
errorType: 'ConflictError',
|
|
287
|
+
statusCode: 409,
|
|
288
|
+
message: 'The server has encountered an conflict.'
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export class MigrationError extends GhostError {
|
|
294
|
+
constructor(options: GhostErrorOptions = {}) {
|
|
295
|
+
super(mergeOptions(options, {
|
|
296
|
+
errorType: 'MigrationError',
|
|
297
|
+
message: 'An error has occurred applying a database migration.',
|
|
298
|
+
level: 'critical'
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {GhostError} from './GhostError';
|
|
2
|
+
import * as ghostErrors from './errors';
|
|
3
|
+
import {deserialize, isGhostError, prepareStackForUser, serialize} from './utils';
|
|
4
|
+
|
|
5
|
+
export * from './errors';
|
|
6
|
+
export type {GhostError};
|
|
7
|
+
export default ghostErrors;
|
|
8
|
+
|
|
9
|
+
export const utils = {
|
|
10
|
+
serialize,
|
|
11
|
+
deserialize,
|
|
12
|
+
isGhostError,
|
|
13
|
+
prepareStackForUser
|
|
14
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import deepCopy from '@stdlib/utils-copy';
|
|
2
|
+
import {GhostError} from './GhostError';
|
|
3
|
+
import * as errors from './errors';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
type AnyObject = Record<string, any>
|
|
7
|
+
|
|
8
|
+
const errorsWithBase: Record<string, typeof GhostError> = {...errors, GhostError};
|
|
9
|
+
|
|
10
|
+
const _private = {
|
|
11
|
+
serialize(err: GhostError) {
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
id: err.id,
|
|
15
|
+
status: err.statusCode,
|
|
16
|
+
code: err.code || err.errorType,
|
|
17
|
+
title: err.name,
|
|
18
|
+
detail: err.message,
|
|
19
|
+
meta: {
|
|
20
|
+
context: err.context,
|
|
21
|
+
help: err.help,
|
|
22
|
+
errorDetails: err.errorDetails,
|
|
23
|
+
level: err.level,
|
|
24
|
+
errorType: err.errorType
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
detail: 'Something went wrong.'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
deserialize(obj: AnyObject) {
|
|
35
|
+
return {
|
|
36
|
+
id: obj.id,
|
|
37
|
+
message: obj.detail || obj.error_description || obj.message,
|
|
38
|
+
statusCode: obj.status,
|
|
39
|
+
code: obj.code || obj.error,
|
|
40
|
+
level: obj.meta && obj.meta.level,
|
|
41
|
+
help: obj.meta && obj.meta.help,
|
|
42
|
+
context: obj.meta && obj.meta.context
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @description Serialize error instance into oauth format.
|
|
48
|
+
*
|
|
49
|
+
* @see https://tools.ietf.org/html/rfc6749#page-45
|
|
50
|
+
*
|
|
51
|
+
* To not loose any error data when sending errors between internal services, we use the suggested OAuth properties and add ours as well.
|
|
52
|
+
*/
|
|
53
|
+
OAuthSerialize(err: GhostError) {
|
|
54
|
+
const matchTable = {
|
|
55
|
+
[errors.NoPermissionError.name]: 'access_denied',
|
|
56
|
+
[errors.MaintenanceError.name]: 'temporarily_unavailable',
|
|
57
|
+
[errors.BadRequestError.name]: 'invalid_request',
|
|
58
|
+
[errors.ValidationError.name]: 'invalid_request',
|
|
59
|
+
default: 'server_error'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
63
|
+
const {detail, code, ...properties} = _private.serialize(err);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
error: err.code || matchTable[err.name] || 'server_error',
|
|
67
|
+
error_description: err.message,
|
|
68
|
+
...properties
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @description Deserialize oauth error format into GhostError instance.
|
|
74
|
+
* @constructor
|
|
75
|
+
*/
|
|
76
|
+
OAuthDeserialize(errorFormat: AnyObject): GhostError {
|
|
77
|
+
try {
|
|
78
|
+
return new errorsWithBase[errorFormat.title || errorFormat.name || errors.InternalServerError.name](_private.deserialize(errorFormat));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// CASE: you receive an OAuth formatted error, but the error prototype is unknown
|
|
81
|
+
return new errors.InternalServerError({
|
|
82
|
+
errorType: errorFormat.title || errorFormat.name,
|
|
83
|
+
..._private.deserialize(errorFormat)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @description Serialize GhostError instance into jsonapi.org format.
|
|
90
|
+
* @param err
|
|
91
|
+
* @return {Object}
|
|
92
|
+
*/
|
|
93
|
+
JSONAPISerialize(err: GhostError): AnyObject {
|
|
94
|
+
const errorFormat: AnyObject = {
|
|
95
|
+
errors: [_private.serialize(err)]
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
errorFormat.errors[0].source = {};
|
|
99
|
+
|
|
100
|
+
if (err.property) {
|
|
101
|
+
errorFormat.errors[0].source.pointer = '/data/attributes/' + err.property;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return errorFormat;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @description Deserialize JSON api format into GhostError instance.
|
|
109
|
+
*/
|
|
110
|
+
JSONAPIDeserialize(errorFormat: AnyObject): GhostError {
|
|
111
|
+
errorFormat = errorFormat.errors && errorFormat.errors[0] || {};
|
|
112
|
+
|
|
113
|
+
let internalError;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
internalError = new errorsWithBase[errorFormat.title || errorFormat.name || errors.InternalServerError.name](_private.deserialize(errorFormat));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// CASE: you receive a JSON format error, but the error prototype is unknown
|
|
119
|
+
internalError = new errors.InternalServerError({
|
|
120
|
+
errorType: errorFormat.title || errorFormat.name,
|
|
121
|
+
..._private.deserialize(errorFormat)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (errorFormat.source && errorFormat.source.pointer) {
|
|
126
|
+
internalError.property = errorFormat.source.pointer.split('/')[3];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return internalError;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @description Serialize GhostError instance to error JSON format
|
|
135
|
+
*
|
|
136
|
+
* jsonapi.org error format:
|
|
137
|
+
*
|
|
138
|
+
* source: {
|
|
139
|
+
* parameter: URL query parameter (no support yet)
|
|
140
|
+
* pointer: HTTP body attribute
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* @see http://jsonapi.org/format/#errors
|
|
144
|
+
*/
|
|
145
|
+
export function serialize(err: GhostError, options?: {format: 'jsonapi' | 'oauth'}) {
|
|
146
|
+
options = options || {format: 'jsonapi'};
|
|
147
|
+
|
|
148
|
+
let errorFormat: AnyObject = {};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (options.format === 'jsonapi') {
|
|
152
|
+
errorFormat = _private.JSONAPISerialize(err);
|
|
153
|
+
} else {
|
|
154
|
+
errorFormat = _private.OAuthSerialize(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
|
+
*/
|
|
167
|
+
export function deserialize(errorFormat: AnyObject): AnyObject {
|
|
168
|
+
let internalError = {};
|
|
169
|
+
|
|
170
|
+
if (errorFormat.errors) {
|
|
171
|
+
internalError = _private.JSONAPIDeserialize(errorFormat);
|
|
172
|
+
} else {
|
|
173
|
+
internalError = _private.OAuthDeserialize(errorFormat);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return internalError;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @description Replace the stack with a user-facing one
|
|
181
|
+
* @returns Clone of the original error with a user-facing stack
|
|
182
|
+
*/
|
|
183
|
+
export function prepareStackForUser(error: GhostError): GhostError
|
|
184
|
+
export function prepareStackForUser(error: Error): Error
|
|
185
|
+
export function prepareStackForUser(error: Error): Error {
|
|
186
|
+
const stackbits = error.stack?.split(/\n/) || [];
|
|
187
|
+
|
|
188
|
+
// We build this up backwards, so we always insert at position 1
|
|
189
|
+
|
|
190
|
+
const hideStack = 'hideStack' in error && error.hideStack;
|
|
191
|
+
|
|
192
|
+
if (process.env.NODE_ENV === 'production' || hideStack) {
|
|
193
|
+
stackbits.splice(1, stackbits.length - 1);
|
|
194
|
+
} else {
|
|
195
|
+
// Clearly mark the strack trace
|
|
196
|
+
stackbits.splice(1, 0, `Stack Trace:`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add in our custom context and help methods
|
|
200
|
+
if ('help' in error && error.help) {
|
|
201
|
+
stackbits.splice(1, 0, `${error.help}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if ('context' in error && error.context) {
|
|
205
|
+
stackbits.splice(1, 0, `${error.context}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// @NOTE: would be a good idea to swap out the cloning implementation with native
|
|
209
|
+
// `structuredClone` one once we use Node v17 or higher. Before making an
|
|
210
|
+
// upgrade make sure structuredClone does a full copy of all properties
|
|
211
|
+
// present on a custom error (see issue: https://github.com/ungap/structured-clone/issues/12)
|
|
212
|
+
const errorClone = deepCopy(error);
|
|
213
|
+
errorClone.stack = stackbits.join('\n');
|
|
214
|
+
return errorClone;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @description Check whether an error instance is a GhostError.
|
|
219
|
+
*/
|
|
220
|
+
export function isGhostError(err: Error) {
|
|
221
|
+
const errorName = GhostError.name;
|
|
222
|
+
const legacyErrorName = 'IgnitionError';
|
|
223
|
+
|
|
224
|
+
const recursiveIsGhostError = function recursiveIsGhostError(obj: AnyObject): boolean {
|
|
225
|
+
// no super constructor available anymore
|
|
226
|
+
if (!obj || !obj.name) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (obj.name === errorName || obj.name === legacyErrorName) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return recursiveIsGhostError(Object.getPrototypeOf(obj));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return recursiveIsGhostError(err.constructor);
|
|
238
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface GhostErrorOptions {
|
|
2
|
+
message?: string;
|
|
3
|
+
statusCode?: number;
|
|
4
|
+
level?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
context?: string;
|
|
7
|
+
help?: string;
|
|
8
|
+
errorType?: string;
|
|
9
|
+
errorDetails?: any;
|
|
10
|
+
code?: string;
|
|
11
|
+
property?: string;
|
|
12
|
+
redirect?: string;
|
|
13
|
+
hideStack?: boolean;
|
|
14
|
+
err?: Error | string;
|
|
15
|
+
}
|
|
16
|
+
export declare class GhostError extends Error {
|
|
17
|
+
statusCode: number;
|
|
18
|
+
errorType: string;
|
|
19
|
+
level: string;
|
|
20
|
+
id: string;
|
|
21
|
+
context?: string;
|
|
22
|
+
help?: string;
|
|
23
|
+
errorDetails: any;
|
|
24
|
+
code: string | null;
|
|
25
|
+
property: string | null;
|
|
26
|
+
redirect: string | null;
|
|
27
|
+
hideStack: boolean;
|
|
28
|
+
constructor(options?: GhostErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=GhostError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GhostError.d.ts","sourceRoot":"","sources":["../src/GhostError.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,iBAAiB;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB;AAED,qBAAa,UAAW,SAAQ,KAAK;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,YAAY,EAAE,GAAG,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;gBAEP,OAAO,GAAE,iBAAsB;CA6D9C"}
|