Skip to content

Commit 47fea13

Browse files
MoLowtargos
authored andcommitted
worker: support more cases when (de)serializing errors
- error.cause is potentially an error, so is now handled recursively - best effort to serialize thrown symbols - handle thrown object with custom inspect PR-URL: #47925 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent a4fed6c commit 47fea13

File tree

2 files changed

+111
-11
lines changed

2 files changed

+111
-11
lines changed

lib/internal/error_serdes.js

+52-11
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,31 @@ const {
1313
ObjectGetOwnPropertyNames,
1414
ObjectGetPrototypeOf,
1515
ObjectKeys,
16+
ObjectPrototypeHasOwnProperty,
1617
ObjectPrototypeToString,
1718
RangeError,
1819
ReferenceError,
1920
SafeSet,
21+
StringFromCharCode,
22+
StringPrototypeSubstring,
2023
SymbolToStringTag,
2124
SyntaxError,
25+
SymbolFor,
2226
TypeError,
27+
TypedArrayPrototypeGetBuffer,
28+
TypedArrayPrototypeGetByteOffset,
29+
TypedArrayPrototypeGetByteLength,
2330
URIError,
2431
} = primordials;
32+
const { inspect: { custom: customInspectSymbol } } = require('util');
2533

2634
const kSerializedError = 0;
2735
const kSerializedObject = 1;
2836
const kInspectedError = 2;
37+
const kInspectedSymbol = 3;
38+
const kCustomInspectedObject = 4;
39+
40+
const kSymbolStringLength = 'Symbol('.length;
2941

3042
const errors = {
3143
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError,
@@ -42,19 +54,24 @@ function TryGetAllProperties(object, target = object) {
4254
ArrayPrototypeForEach(keys, (key) => {
4355
let descriptor;
4456
try {
57+
// TODO: create a null-prototype descriptor with needed properties only
4558
descriptor = ObjectGetOwnPropertyDescriptor(object, key);
4659
} catch { return; }
4760
const getter = descriptor.get;
4861
if (getter && key !== '__proto__') {
4962
try {
5063
descriptor.value = FunctionPrototypeCall(getter, target);
64+
delete descriptor.get;
65+
delete descriptor.set;
5166
} catch {
5267
// Continue regardless of error.
5368
}
5469
}
55-
if ('value' in descriptor && typeof descriptor.value !== 'function') {
56-
delete descriptor.get;
57-
delete descriptor.set;
70+
if (key === 'cause') {
71+
descriptor.value = serializeError(descriptor.value);
72+
all[key] = descriptor;
73+
} else if ('value' in descriptor &&
74+
typeof descriptor.value !== 'function' && typeof descriptor.value !== 'symbol') {
5875
all[key] = descriptor;
5976
}
6077
});
@@ -95,6 +112,9 @@ function inspect(...args) {
95112
let serialize;
96113
function serializeError(error) {
97114
if (!serialize) serialize = require('v8').serialize;
115+
if (typeof error === 'symbol') {
116+
return Buffer.from(StringFromCharCode(kInspectedSymbol) + inspect(error), 'utf8');
117+
}
98118
try {
99119
if (typeof error === 'object' &&
100120
ObjectPrototypeToString(error) === '[object Error]') {
@@ -113,14 +133,27 @@ function serializeError(error) {
113133
} catch {
114134
// Continue regardless of error.
115135
}
136+
try {
137+
if (error != null &&
138+
ObjectPrototypeHasOwnProperty(error, customInspectSymbol)) {
139+
return Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), 'utf8');
140+
}
141+
} catch {
142+
// Continue regardless of error.
143+
}
116144
try {
117145
const serialized = serialize(error);
118146
return Buffer.concat([Buffer.from([kSerializedObject]), serialized]);
119147
} catch {
120148
// Continue regardless of error.
121149
}
122-
return Buffer.concat([Buffer.from([kInspectedError]),
123-
Buffer.from(inspect(error), 'utf8')]);
150+
return Buffer.from(StringFromCharCode(kInspectedError) + inspect(error), 'utf8');
151+
}
152+
153+
function fromBuffer(error) {
154+
return Buffer.from(TypedArrayPrototypeGetBuffer(error),
155+
TypedArrayPrototypeGetByteOffset(error) + 1,
156+
TypedArrayPrototypeGetByteLength(error) - 1);
124157
}
125158

126159
let deserialize;
@@ -132,19 +165,27 @@ function deserializeError(error) {
132165
const ctor = errors[constructor];
133166
ObjectDefineProperty(properties, SymbolToStringTag, {
134167
__proto__: null,
135-
value: { value: 'Error', configurable: true },
168+
value: { __proto__: null, value: 'Error', configurable: true },
136169
enumerable: true,
137170
});
171+
if ('cause' in properties && 'value' in properties.cause) {
172+
properties.cause.value = deserializeError(properties.cause.value);
173+
}
138174
return ObjectCreate(ctor.prototype, properties);
139175
}
140176
case kSerializedObject:
141177
return deserialize(error.subarray(1));
142-
case kInspectedError: {
143-
const buf = Buffer.from(error.buffer,
144-
error.byteOffset + 1,
145-
error.byteLength - 1);
146-
return buf.toString('utf8');
178+
case kInspectedError:
179+
return fromBuffer(error).toString('utf8');
180+
case kInspectedSymbol: {
181+
const buf = fromBuffer(error);
182+
return SymbolFor(StringPrototypeSubstring(buf.toString('utf8'), kSymbolStringLength, buf.length - 1));
147183
}
184+
case kCustomInspectedObject:
185+
return {
186+
__proto__: null,
187+
[customInspectSymbol]: () => fromBuffer(error).toString('utf8'),
188+
};
148189
}
149190
require('assert').fail('This should not happen');
150191
}

test/parallel/test-error-serdes.js

+59
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
'use strict';
33
require('../common');
44
const assert = require('assert');
5+
const { inspect } = require('util');
56
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
67
const { serializeError, deserializeError } = require('internal/error_serdes');
78

@@ -15,6 +16,9 @@ assert.strictEqual(cycle(1.4), 1.4);
1516
assert.strictEqual(cycle(null), null);
1617
assert.strictEqual(cycle(undefined), undefined);
1718
assert.strictEqual(cycle('foo'), 'foo');
19+
assert.strictEqual(cycle(Symbol.for('foo')), Symbol.for('foo'));
20+
assert.strictEqual(cycle(Symbol('foo')).toString(), Symbol('foo').toString());
21+
1822

1923
let err = new Error('foo');
2024
for (let i = 0; i < 10; i++) {
@@ -43,6 +47,52 @@ assert.strictEqual(cycle(new SubError('foo')).name, 'Error');
4347
assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' });
4448
assert.strictEqual(cycle(Function), '[Function: Function]');
4549

50+
class ErrorWithCause extends Error {
51+
get cause() {
52+
return new Error('err');
53+
}
54+
}
55+
class ErrorWithThowingCause extends Error {
56+
get cause() {
57+
throw new Error('err');
58+
}
59+
}
60+
class ErrorWithCyclicCause extends Error {
61+
get cause() {
62+
return new ErrorWithCyclicCause();
63+
}
64+
}
65+
const errorWithCause = Object
66+
.defineProperty(new Error('Error with cause'), 'cause', { get() { return { foo: 'bar' }; } });
67+
const errorWithThrowingCause = Object
68+
.defineProperty(new Error('Error with cause'), 'cause', { get() { throw new Error('err'); } });
69+
const errorWithCyclicCause = Object
70+
.defineProperty(new Error('Error with cause'), 'cause', { get() { return errorWithCyclicCause; } });
71+
72+
assert.strictEqual(cycle(new Error('Error with cause', { cause: 0 })).cause, 0);
73+
assert.strictEqual(cycle(new Error('Error with cause', { cause: -1 })).cause, -1);
74+
assert.strictEqual(cycle(new Error('Error with cause', { cause: 1.4 })).cause, 1.4);
75+
assert.strictEqual(cycle(new Error('Error with cause', { cause: null })).cause, null);
76+
assert.strictEqual(cycle(new Error('Error with cause', { cause: undefined })).cause, undefined);
77+
assert.strictEqual(Object.hasOwn(cycle(new Error('Error with cause', { cause: undefined })), 'cause'), true);
78+
assert.strictEqual(cycle(new Error('Error with cause', { cause: 'foo' })).cause, 'foo');
79+
assert.deepStrictEqual(cycle(new Error('Error with cause', { cause: new Error('err') })).cause, new Error('err'));
80+
assert.deepStrictEqual(cycle(errorWithCause).cause, { foo: 'bar' });
81+
assert.strictEqual(Object.hasOwn(cycle(errorWithThrowingCause), 'cause'), false);
82+
assert.strictEqual(Object.hasOwn(cycle(errorWithCyclicCause), 'cause'), true);
83+
assert.deepStrictEqual(cycle(new ErrorWithCause('Error with cause')).cause, new Error('err'));
84+
assert.strictEqual(cycle(new ErrorWithThowingCause('Error with cause')).cause, undefined);
85+
assert.strictEqual(Object.hasOwn(cycle(new ErrorWithThowingCause('Error with cause')), 'cause'), false);
86+
// When the cause is cyclic, it is serialized until Maxiumum call stack size is reached
87+
let depth = 0;
88+
let e = cycle(new ErrorWithCyclicCause('Error with cause'));
89+
while (e.cause) {
90+
e = e.cause;
91+
depth++;
92+
}
93+
assert(depth > 1);
94+
95+
4696
{
4797
const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42);
4898
assert.match(String(err), /^TypeError \[ERR_INVALID_ARG_TYPE\]:/);
@@ -66,3 +116,12 @@ assert.strictEqual(cycle(Function), '[Function: Function]');
66116
serializeError(new DynamicError());
67117
assert.strictEqual(called, true);
68118
}
119+
120+
121+
const data = {
122+
foo: 'bar',
123+
[inspect.custom]() {
124+
return 'barbaz';
125+
}
126+
};
127+
assert.strictEqual(inspect(cycle(data)), 'barbaz');

0 commit comments

Comments
 (0)