-
Notifications
You must be signed in to change notification settings - Fork 72
/
tame-error-constructor.js
295 lines (279 loc) · 9.42 KB
/
tame-error-constructor.js
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import {
FERAL_ERROR,
TypeError,
apply,
construct,
defineProperties,
setPrototypeOf,
getOwnPropertyDescriptor,
defineProperty,
getOwnPropertyDescriptors,
} from '../commons.js';
import { NativeErrors } from '../permits.js';
import { tameV8ErrorConstructor } from './tame-v8-error-constructor.js';
// Present on at least FF and XS. Proposed by Error-proposal. The original
// is dangerous, so tameErrorConstructor replaces it with a safe one.
// We grab the original here before it gets replaced.
const stackDesc = getOwnPropertyDescriptor(FERAL_ERROR.prototype, 'stack');
const stackGetter = stackDesc && stackDesc.get;
// Use concise methods to obtain named functions without constructors.
const tamedMethods = {
getStackString(error) {
if (typeof stackGetter === 'function') {
return apply(stackGetter, error, []);
} else if ('stack' in error) {
// The fallback is to just use the de facto `error.stack` if present
return `${error.stack}`;
}
return '';
},
};
let initialGetStackString = tamedMethods.getStackString;
export default function tameErrorConstructor(
errorTaming = 'safe',
stackFiltering = 'concise',
) {
if (
errorTaming !== 'safe' &&
errorTaming !== 'unsafe' &&
errorTaming !== 'unsafe-debug'
) {
throw TypeError(`unrecognized errorTaming ${errorTaming}`);
}
if (stackFiltering !== 'concise' && stackFiltering !== 'verbose') {
throw TypeError(`unrecognized stackFiltering ${stackFiltering}`);
}
const ErrorPrototype = FERAL_ERROR.prototype;
const { captureStackTrace: originalCaptureStackTrace } = FERAL_ERROR;
const platform =
typeof originalCaptureStackTrace === 'function' ? 'v8' : 'unknown';
const makeErrorConstructor = (_ = {}) => {
// eslint-disable-next-line no-shadow
const ResultError = function Error(...rest) {
let error;
if (new.target === undefined) {
error = apply(FERAL_ERROR, this, rest);
} else {
error = construct(FERAL_ERROR, rest, new.target);
}
if (platform === 'v8') {
// TODO Likely expensive!
apply(originalCaptureStackTrace, FERAL_ERROR, [error, ResultError]);
}
return error;
};
defineProperties(ResultError, {
length: { value: 1 },
prototype: {
value: ErrorPrototype,
writable: false,
enumerable: false,
configurable: false,
},
});
return ResultError;
};
const InitialError = makeErrorConstructor({ powers: 'original' });
const SharedError = makeErrorConstructor({ powers: 'none' });
defineProperties(ErrorPrototype, {
constructor: { value: SharedError },
});
for (const NativeError of NativeErrors) {
setPrototypeOf(NativeError, SharedError);
}
// https://v8.dev/docs/stack-trace-api#compatibility advises that
// programmers can "always" set `Error.stackTraceLimit`
// even on non-v8 platforms. On non-v8
// it will have no effect, but this advice only makes sense
// if the assignment itself does not fail, which it would
// if `Error` were naively frozen. Hence, we add setters that
// accept but ignore the assignment on non-v8 platforms.
defineProperties(InitialError, {
stackTraceLimit: {
get() {
if (typeof FERAL_ERROR.stackTraceLimit === 'number') {
// FERAL_ERROR.stackTraceLimit is only on v8
return FERAL_ERROR.stackTraceLimit;
}
return undefined;
},
set(newLimit) {
if (typeof newLimit !== 'number') {
// silently do nothing. This behavior doesn't precisely
// emulate v8 edge-case behavior. But given the purpose
// of this emulation, having edge cases err towards
// harmless seems the safer option.
return;
}
if (typeof FERAL_ERROR.stackTraceLimit === 'number') {
// FERAL_ERROR.stackTraceLimit is only on v8
FERAL_ERROR.stackTraceLimit = newLimit;
// We place the useless return on the next line to ensure
// that anything we place after the if in the future only
// happens if the then-case does not.
// eslint-disable-next-line no-useless-return
return;
}
},
// WTF on v8 stackTraceLimit is enumerable
enumerable: false,
configurable: true,
},
});
if (errorTaming === 'unsafe-debug' && platform === 'v8') {
// This case is a kludge to work around
// https://github.com/endojs/endo/issues/1798
// https://github.com/endojs/endo/issues/2348
// https://github.com/Agoric/agoric-sdk/issues/8662
defineProperties(InitialError, {
prepareStackTrace: {
get() {
return FERAL_ERROR.prepareStackTrace;
},
set(newPrepareStackTrace) {
FERAL_ERROR.prepareStackTrace = newPrepareStackTrace;
},
enumerable: false,
configurable: true,
},
captureStackTrace: {
value: FERAL_ERROR.captureStackTrace,
writable: true,
enumerable: false,
configurable: true,
},
});
const descs = getOwnPropertyDescriptors(InitialError);
defineProperties(SharedError, {
stackTraceLimit: descs.stackTraceLimit,
prepareStackTrace: descs.prepareStackTrace,
captureStackTrace: descs.captureStackTrace,
});
return {
'%InitialGetStackString%': initialGetStackString,
'%InitialError%': InitialError,
'%SharedError%': SharedError,
};
}
// The default SharedError much be completely powerless even on v8,
// so the lenient `stackTraceLimit` accessor does nothing on all
// platforms.
defineProperties(SharedError, {
stackTraceLimit: {
get() {
return undefined;
},
set(_newLimit) {
// do nothing
},
enumerable: false,
configurable: true,
},
});
if (platform === 'v8') {
// `SharedError.prepareStackTrace`, if it exists, must also be
// powerless. However, from what we've heard, depd expects to be able to
// assign to it without the assignment throwing. It is normally a function
// that returns a stack string to be magically added to error objects.
// However, as long as we're adding a lenient standin, we may as well
// accommodate any who expect to get a function they can call and get
// a string back. This prepareStackTrace is a do-nothing function that
// always returns the empty string.
defineProperties(SharedError, {
prepareStackTrace: {
get() {
return () => '';
},
set(_prepareFn) {
// do nothing
},
enumerable: false,
configurable: true,
},
captureStackTrace: {
value: (errorish, _constructorOpt) => {
defineProperty(errorish, 'stack', {
value: '',
});
},
writable: false,
enumerable: false,
configurable: true,
},
});
}
if (platform === 'v8') {
initialGetStackString = tameV8ErrorConstructor(
FERAL_ERROR,
InitialError,
errorTaming,
stackFiltering,
);
} else if (errorTaming === 'unsafe' || errorTaming === 'unsafe-debug') {
// v8 has too much magic around their 'stack' own property for it to
// coexist cleanly with this accessor. So only install it on non-v8
// Error.prototype.stack property as proposed at
// https://tc39.es/proposal-error-stacks/
// with the fix proposed at
// https://github.com/tc39/proposal-error-stacks/issues/46
// On others, this still protects from the override mistake,
// essentially like enable-property-overrides.js would
// once this accessor property itself is frozen, as will happen
// later during lockdown.
//
// However, there is here a change from the intent in the current
// state of the proposal. If experience tells us whether this change
// is a good idea, we should modify the proposal accordingly. There is
// much code in the world that assumes `error.stack` is a string. So
// where the proposal accommodates secure operation by making the
// property optional, we instead accommodate secure operation by
// having the secure form always return only the stable part, the
// stringified error instance, and omitting all the frame information
// rather than omitting the property.
defineProperties(ErrorPrototype, {
stack: {
get() {
return initialGetStackString(this);
},
set(newValue) {
defineProperties(this, {
stack: {
value: newValue,
writable: true,
enumerable: true,
configurable: true,
},
});
},
},
});
} else {
// v8 has too much magic around their 'stack' own property for it to
// coexist cleanly with this accessor. So only install it on non-v8
defineProperties(ErrorPrototype, {
stack: {
get() {
// https://github.com/tc39/proposal-error-stacks/issues/46
// allows this to not add an unpleasant newline. Otherwise
// we should fix this.
return `${this}`;
},
set(newValue) {
defineProperties(this, {
stack: {
value: newValue,
writable: true,
enumerable: true,
configurable: true,
},
});
},
},
});
}
return {
'%InitialGetStackString%': initialGetStackString,
'%InitialError%': InitialError,
'%SharedError%': SharedError,
};
}