Skip to content

Commit 9ad42b7

Browse files
addaleaxtargos
authored andcommitted
worker: improve error (de)serialization
Rather than passing errors using some sort of string representation, do a best effort for faithful serialization/deserialization of uncaught exception objects. PR-URL: #20876 Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Shingo Inoue <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]> Reviewed-By: John-David Dalton <[email protected]> Reviewed-By: Gus Caplan <[email protected]>
1 parent ecba1c5 commit 9ad42b7

6 files changed

+171
-16
lines changed

lib/internal/error-serdes.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
const Buffer = require('buffer').Buffer;
4+
const { serialize, deserialize } = require('v8');
5+
const { SafeSet } = require('internal/safe_globals');
6+
7+
const kSerializedError = 0;
8+
const kSerializedObject = 1;
9+
const kInspectedError = 2;
10+
11+
const GetPrototypeOf = Object.getPrototypeOf;
12+
const GetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
13+
const GetOwnPropertyNames = Object.getOwnPropertyNames;
14+
const DefineProperty = Object.defineProperty;
15+
const Assign = Object.assign;
16+
const ObjectPrototypeToString =
17+
Function.prototype.call.bind(Object.prototype.toString);
18+
const ForEach = Function.prototype.call.bind(Array.prototype.forEach);
19+
const Call = Function.prototype.call.bind(Function.prototype.call);
20+
21+
const errors = {
22+
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError
23+
};
24+
const errorConstructorNames = new SafeSet(Object.keys(errors));
25+
26+
function TryGetAllProperties(object, target = object) {
27+
const all = Object.create(null);
28+
if (object === null)
29+
return all;
30+
Assign(all, TryGetAllProperties(GetPrototypeOf(object), target));
31+
const keys = GetOwnPropertyNames(object);
32+
ForEach(keys, (key) => {
33+
const descriptor = GetOwnPropertyDescriptor(object, key);
34+
const getter = descriptor.get;
35+
if (getter && key !== '__proto__') {
36+
try {
37+
descriptor.value = Call(getter, target);
38+
} catch {}
39+
}
40+
if ('value' in descriptor && typeof descriptor.value !== 'function') {
41+
delete descriptor.get;
42+
delete descriptor.set;
43+
all[key] = descriptor;
44+
}
45+
});
46+
return all;
47+
}
48+
49+
function GetConstructors(object) {
50+
const constructors = [];
51+
52+
for (var current = object;
53+
current !== null;
54+
current = GetPrototypeOf(current)) {
55+
const desc = GetOwnPropertyDescriptor(current, 'constructor');
56+
if (desc && desc.value) {
57+
DefineProperty(constructors, constructors.length, {
58+
value: desc.value, enumerable: true
59+
});
60+
}
61+
}
62+
63+
return constructors;
64+
}
65+
66+
function GetName(object) {
67+
const desc = GetOwnPropertyDescriptor(object, 'name');
68+
return desc && desc.value;
69+
}
70+
71+
let util;
72+
function lazyUtil() {
73+
if (!util)
74+
util = require('util');
75+
return util;
76+
}
77+
78+
function serializeError(error) {
79+
try {
80+
if (typeof error === 'object' &&
81+
ObjectPrototypeToString(error) === '[object Error]') {
82+
const constructors = GetConstructors(error);
83+
for (var i = constructors.length - 1; i >= 0; i--) {
84+
const name = GetName(constructors[i]);
85+
if (errorConstructorNames.has(name)) {
86+
try { error.stack; } catch {}
87+
const serialized = serialize({
88+
constructor: name,
89+
properties: TryGetAllProperties(error)
90+
});
91+
return Buffer.concat([Buffer.from([kSerializedError]), serialized]);
92+
}
93+
}
94+
}
95+
} catch {}
96+
try {
97+
const serialized = serialize(error);
98+
return Buffer.concat([Buffer.from([kSerializedObject]), serialized]);
99+
} catch {}
100+
return Buffer.concat([Buffer.from([kInspectedError]),
101+
Buffer.from(lazyUtil().inspect(error), 'utf8')]);
102+
}
103+
104+
function deserializeError(error) {
105+
switch (error[0]) {
106+
case kSerializedError:
107+
const { constructor, properties } = deserialize(error.subarray(1));
108+
const ctor = errors[constructor];
109+
return Object.create(ctor.prototype, properties);
110+
case kSerializedObject:
111+
return deserialize(error.subarray(1));
112+
case kInspectedError:
113+
const buf = Buffer.from(error.buffer,
114+
error.byteOffset + 1,
115+
error.byteLength - 1);
116+
return buf.toString('utf8');
117+
}
118+
require('assert').fail('This should not happen');
119+
}
120+
121+
module.exports = { serializeError, deserializeError };

lib/internal/worker.js

+1-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
const Buffer = require('buffer').Buffer;
43
const EventEmitter = require('events');
54
const assert = require('assert');
65
const path = require('path');
@@ -17,6 +16,7 @@ const { internalBinding } = require('internal/bootstrap/loaders');
1716
const { MessagePort, MessageChannel } = internalBinding('messaging');
1817
const { handle_onclose } = internalBinding('symbols');
1918
const { clearAsyncIdStack } = require('internal/async_hooks');
19+
const { serializeError, deserializeError } = require('internal/error-serdes');
2020

2121
util.inherits(MessagePort, EventEmitter);
2222

@@ -453,17 +453,6 @@ function setupChild(evalScript) {
453453
}
454454
}
455455

456-
// TODO(addaleax): These can be improved a lot.
457-
function serializeError(error) {
458-
return Buffer.from(util.inspect(error), 'utf8');
459-
}
460-
461-
function deserializeError(error) {
462-
return Buffer.from(error.buffer,
463-
error.byteOffset,
464-
error.byteLength).toString('utf8');
465-
}
466-
467456
function pipeWithoutWarning(source, dest) {
468457
const sourceMaxListeners = source._maxListeners;
469458
const destMaxListeners = dest._maxListeners;

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
'lib/internal/constants.js',
103103
'lib/internal/encoding.js',
104104
'lib/internal/errors.js',
105+
'lib/internal/error-serdes.js',
105106
'lib/internal/fixed_queue.js',
106107
'lib/internal/freelist.js',
107108
'lib/internal/fs/promises.js',

test/parallel/test-error-serdes.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
require('../common');
4+
const assert = require('assert');
5+
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
6+
const { serializeError, deserializeError } = require('internal/error-serdes');
7+
8+
function cycle(err) {
9+
return deserializeError(serializeError(err));
10+
}
11+
12+
assert.strictEqual(cycle(0), 0);
13+
assert.strictEqual(cycle(-1), -1);
14+
assert.strictEqual(cycle(1.4), 1.4);
15+
assert.strictEqual(cycle(null), null);
16+
assert.strictEqual(cycle(undefined), undefined);
17+
assert.strictEqual(cycle('foo'), 'foo');
18+
19+
{
20+
const err = cycle(new Error('foo'));
21+
assert(err instanceof Error);
22+
assert.strictEqual(err.name, 'Error');
23+
assert.strictEqual(err.message, 'foo');
24+
assert(/^Error: foo\n/.test(err.stack));
25+
}
26+
27+
assert.strictEqual(cycle(new RangeError('foo')).name, 'RangeError');
28+
assert.strictEqual(cycle(new TypeError('foo')).name, 'TypeError');
29+
assert.strictEqual(cycle(new ReferenceError('foo')).name, 'ReferenceError');
30+
assert.strictEqual(cycle(new URIError('foo')).name, 'URIError');
31+
assert.strictEqual(cycle(new EvalError('foo')).name, 'EvalError');
32+
assert.strictEqual(cycle(new SyntaxError('foo')).name, 'SyntaxError');
33+
34+
class SubError extends Error {}
35+
36+
assert.strictEqual(cycle(new SubError('foo')).name, 'Error');
37+
38+
assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' });
39+
assert.strictEqual(cycle(Function), '[Function: Function]');
40+
41+
{
42+
const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42);
43+
assert(/^TypeError \[ERR_INVALID_ARG_TYPE\]:/.test(err));
44+
assert.strictEqual(err.name, 'TypeError [ERR_INVALID_ARG_TYPE]');
45+
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
46+
}

test/parallel/test-worker-uncaught-exception-async.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
1010
const w = new Worker(__filename);
1111
w.on('message', common.mustNotCall());
1212
w.on('error', common.mustCall((err) => {
13-
// TODO(addaleax): be more specific here
14-
assert(/foo/.test(err));
13+
assert(/^Error: foo$/.test(err));
1514
}));
1615
} else {
1716
setImmediate(() => {

test/parallel/test-worker-uncaught-exception.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
1010
const w = new Worker(__filename);
1111
w.on('message', common.mustNotCall());
1212
w.on('error', common.mustCall((err) => {
13-
// TODO(addaleax): be more specific here
14-
assert(/foo/.test(err));
13+
assert(/^Error: foo$/.test(err));
1514
}));
1615
} else {
1716
throw new Error('foo');

0 commit comments

Comments
 (0)