-
Notifications
You must be signed in to change notification settings - Fork 41
/
APIError.ts
executable file
·132 lines (111 loc) · 3.77 KB
/
APIError.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { UrlTemplates } from './index';
export type APIErrorJSON = {
status?: string;
code?: string;
title?: string;
detail?: string;
links?: any;
source?: { pointer?: string, parameter?: string };
meta?: object;
};
export type Opts = {
status?: string | number;
title?: string;
detail?: string;
typeUri?: string;
source?: { pointer?: string, parameter?: string };
meta?: object;
rawError?: Error;
};
export const displaySafe = Symbol("isJSONAPIDisplayReady");
export default class APIError extends Error {
public status?: string;
public title?: string;
public detail?: string;
public source?: Opts['source'];
public meta?: object;
// shape may change. To read for now, call toJSON() and examine `code`.
protected typeUri?: string;
// Even though an APIError is ready for display, the user may
// want to process it further (to add more tailed messages, e.g.),
// so we retain the raw error the APIError was created from (if any)
// for use in such processing, but we don't serialize it
public rawError?: Error;
constructor(opts: Opts = {}) {
// Extract title specially for super call.
// Call it with a spread so that arguments in the callee is empty
// (rather than length 1 with [0] === undefined) when no title is present.
super(...(opts.title ? [String(opts.title)] : []));
if(Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor || APIError);
}
// Use a Proxy to handle coercing status, code etc.
// to strings on set (including below in the constructor).
// Because we're setting the validated properties directly on obj,
// ownKeys will work correctly.
const res = new Proxy(this, {
set(obj, prop, value) {
const coercePropToString =
["status", "typeUri", "title", "detail"].indexOf(<string>prop) > -1;
(obj as any)[prop] = coercePropToString
? value == null ? undefined : String(value)
: value;
return true;
}
});
// Construct from object format
Object.assign(res, opts);
return res;
}
toJSON(urlTemplates?: UrlTemplates): APIErrorJSON {
const { rawError, typeUri, ...serializableProps } = this as any;
const res = {
...serializableProps,
...(typeUri ? { code: typeUri } : {})
};
if(urlTemplates && urlTemplates.about && !(res.links && res.links.about)) {
return {
...res,
links: { about: urlTemplates.about(this) }
};
}
return res;
}
/**
* Creates a JSON-API Compliant Error Object from a JS Error object
*
*/
static fromError(err: any) {
const ErrorConstructor = this || APIError; // in case `this` isn't bound.
const fallbackTitle =
"An unknown error occurred while trying to process this request.";
if(err instanceof APIError) {
return err;
}
// If the error is marked as ready for JSON API display, it's secure
// to read values off it and show them to the user. (Note: most of
// the args below will probably be null/undefined, but that's fine.)
else if(this.isDisplaySafe(err)) {
return new ErrorConstructor({
status: err.status || err.statusCode || 500,
title: err.title || fallbackTitle,
detail: err.detail || err.details || (err.message || undefined),
typeUri: err.typeUri,
source: typeof err.source === "object" ? err.source : undefined,
meta: typeof err.meta === "object" ? err.meta : undefined,
rawError: err
});
}
// Otherwise, we just show a generic error message.
else {
return new ErrorConstructor({
status: 500,
title: fallbackTitle,
rawError: err
});
}
}
static isDisplaySafe(it: any) {
return it && (it instanceof APIError || it[displaySafe]);
}
}