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: https://github.com/nodejs/node/pull/20876 Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Shingo Inoue <leko.noor@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com> Reviewed-By: John-David Dalton <john.david.dalton@gmail.com> Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
parent
6d59bfb4f8
commit
d1c096cc9d
121
lib/internal/error-serdes.js
Normal file
121
lib/internal/error-serdes.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Buffer = require('buffer').Buffer;
|
||||||
|
const { serialize, deserialize } = require('v8');
|
||||||
|
const { SafeSet } = require('internal/safe_globals');
|
||||||
|
|
||||||
|
const kSerializedError = 0;
|
||||||
|
const kSerializedObject = 1;
|
||||||
|
const kInspectedError = 2;
|
||||||
|
|
||||||
|
const GetPrototypeOf = Object.getPrototypeOf;
|
||||||
|
const GetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
||||||
|
const GetOwnPropertyNames = Object.getOwnPropertyNames;
|
||||||
|
const DefineProperty = Object.defineProperty;
|
||||||
|
const Assign = Object.assign;
|
||||||
|
const ObjectPrototypeToString =
|
||||||
|
Function.prototype.call.bind(Object.prototype.toString);
|
||||||
|
const ForEach = Function.prototype.call.bind(Array.prototype.forEach);
|
||||||
|
const Call = Function.prototype.call.bind(Function.prototype.call);
|
||||||
|
|
||||||
|
const errors = {
|
||||||
|
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError
|
||||||
|
};
|
||||||
|
const errorConstructorNames = new SafeSet(Object.keys(errors));
|
||||||
|
|
||||||
|
function TryGetAllProperties(object, target = object) {
|
||||||
|
const all = Object.create(null);
|
||||||
|
if (object === null)
|
||||||
|
return all;
|
||||||
|
Assign(all, TryGetAllProperties(GetPrototypeOf(object), target));
|
||||||
|
const keys = GetOwnPropertyNames(object);
|
||||||
|
ForEach(keys, (key) => {
|
||||||
|
const descriptor = GetOwnPropertyDescriptor(object, key);
|
||||||
|
const getter = descriptor.get;
|
||||||
|
if (getter && key !== '__proto__') {
|
||||||
|
try {
|
||||||
|
descriptor.value = Call(getter, target);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if ('value' in descriptor && typeof descriptor.value !== 'function') {
|
||||||
|
delete descriptor.get;
|
||||||
|
delete descriptor.set;
|
||||||
|
all[key] = descriptor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetConstructors(object) {
|
||||||
|
const constructors = [];
|
||||||
|
|
||||||
|
for (var current = object;
|
||||||
|
current !== null;
|
||||||
|
current = GetPrototypeOf(current)) {
|
||||||
|
const desc = GetOwnPropertyDescriptor(current, 'constructor');
|
||||||
|
if (desc && desc.value) {
|
||||||
|
DefineProperty(constructors, constructors.length, {
|
||||||
|
value: desc.value, enumerable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return constructors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetName(object) {
|
||||||
|
const desc = GetOwnPropertyDescriptor(object, 'name');
|
||||||
|
return desc && desc.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let util;
|
||||||
|
function lazyUtil() {
|
||||||
|
if (!util)
|
||||||
|
util = require('util');
|
||||||
|
return util;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeError(error) {
|
||||||
|
try {
|
||||||
|
if (typeof error === 'object' &&
|
||||||
|
ObjectPrototypeToString(error) === '[object Error]') {
|
||||||
|
const constructors = GetConstructors(error);
|
||||||
|
for (var i = constructors.length - 1; i >= 0; i--) {
|
||||||
|
const name = GetName(constructors[i]);
|
||||||
|
if (errorConstructorNames.has(name)) {
|
||||||
|
try { error.stack; } catch {}
|
||||||
|
const serialized = serialize({
|
||||||
|
constructor: name,
|
||||||
|
properties: TryGetAllProperties(error)
|
||||||
|
});
|
||||||
|
return Buffer.concat([Buffer.from([kSerializedError]), serialized]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const serialized = serialize(error);
|
||||||
|
return Buffer.concat([Buffer.from([kSerializedObject]), serialized]);
|
||||||
|
} catch {}
|
||||||
|
return Buffer.concat([Buffer.from([kInspectedError]),
|
||||||
|
Buffer.from(lazyUtil().inspect(error), 'utf8')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deserializeError(error) {
|
||||||
|
switch (error[0]) {
|
||||||
|
case kSerializedError:
|
||||||
|
const { constructor, properties } = deserialize(error.subarray(1));
|
||||||
|
const ctor = errors[constructor];
|
||||||
|
return Object.create(ctor.prototype, properties);
|
||||||
|
case kSerializedObject:
|
||||||
|
return deserialize(error.subarray(1));
|
||||||
|
case kInspectedError:
|
||||||
|
const buf = Buffer.from(error.buffer,
|
||||||
|
error.byteOffset + 1,
|
||||||
|
error.byteLength - 1);
|
||||||
|
return buf.toString('utf8');
|
||||||
|
}
|
||||||
|
require('assert').fail('This should not happen');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { serializeError, deserializeError };
|
@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Buffer = require('buffer').Buffer;
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@ -17,6 +16,7 @@ const { internalBinding } = require('internal/bootstrap/loaders');
|
|||||||
const { MessagePort, MessageChannel } = internalBinding('messaging');
|
const { MessagePort, MessageChannel } = internalBinding('messaging');
|
||||||
const { handle_onclose } = internalBinding('symbols');
|
const { handle_onclose } = internalBinding('symbols');
|
||||||
const { clearAsyncIdStack } = require('internal/async_hooks');
|
const { clearAsyncIdStack } = require('internal/async_hooks');
|
||||||
|
const { serializeError, deserializeError } = require('internal/error-serdes');
|
||||||
|
|
||||||
util.inherits(MessagePort, EventEmitter);
|
util.inherits(MessagePort, EventEmitter);
|
||||||
|
|
||||||
@ -453,17 +453,6 @@ function setupChild(evalScript) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(addaleax): These can be improved a lot.
|
|
||||||
function serializeError(error) {
|
|
||||||
return Buffer.from(util.inspect(error), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeError(error) {
|
|
||||||
return Buffer.from(error.buffer,
|
|
||||||
error.byteOffset,
|
|
||||||
error.byteLength).toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pipeWithoutWarning(source, dest) {
|
function pipeWithoutWarning(source, dest) {
|
||||||
const sourceMaxListeners = source._maxListeners;
|
const sourceMaxListeners = source._maxListeners;
|
||||||
const destMaxListeners = dest._maxListeners;
|
const destMaxListeners = dest._maxListeners;
|
||||||
|
1
node.gyp
1
node.gyp
@ -102,6 +102,7 @@
|
|||||||
'lib/internal/constants.js',
|
'lib/internal/constants.js',
|
||||||
'lib/internal/encoding.js',
|
'lib/internal/encoding.js',
|
||||||
'lib/internal/errors.js',
|
'lib/internal/errors.js',
|
||||||
|
'lib/internal/error-serdes.js',
|
||||||
'lib/internal/fixed_queue.js',
|
'lib/internal/fixed_queue.js',
|
||||||
'lib/internal/freelist.js',
|
'lib/internal/freelist.js',
|
||||||
'lib/internal/fs/promises.js',
|
'lib/internal/fs/promises.js',
|
||||||
|
46
test/parallel/test-error-serdes.js
Normal file
46
test/parallel/test-error-serdes.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Flags: --expose-internals
|
||||||
|
'use strict';
|
||||||
|
require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
|
||||||
|
const { serializeError, deserializeError } = require('internal/error-serdes');
|
||||||
|
|
||||||
|
function cycle(err) {
|
||||||
|
return deserializeError(serializeError(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(cycle(0), 0);
|
||||||
|
assert.strictEqual(cycle(-1), -1);
|
||||||
|
assert.strictEqual(cycle(1.4), 1.4);
|
||||||
|
assert.strictEqual(cycle(null), null);
|
||||||
|
assert.strictEqual(cycle(undefined), undefined);
|
||||||
|
assert.strictEqual(cycle('foo'), 'foo');
|
||||||
|
|
||||||
|
{
|
||||||
|
const err = cycle(new Error('foo'));
|
||||||
|
assert(err instanceof Error);
|
||||||
|
assert.strictEqual(err.name, 'Error');
|
||||||
|
assert.strictEqual(err.message, 'foo');
|
||||||
|
assert(/^Error: foo\n/.test(err.stack));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(cycle(new RangeError('foo')).name, 'RangeError');
|
||||||
|
assert.strictEqual(cycle(new TypeError('foo')).name, 'TypeError');
|
||||||
|
assert.strictEqual(cycle(new ReferenceError('foo')).name, 'ReferenceError');
|
||||||
|
assert.strictEqual(cycle(new URIError('foo')).name, 'URIError');
|
||||||
|
assert.strictEqual(cycle(new EvalError('foo')).name, 'EvalError');
|
||||||
|
assert.strictEqual(cycle(new SyntaxError('foo')).name, 'SyntaxError');
|
||||||
|
|
||||||
|
class SubError extends Error {}
|
||||||
|
|
||||||
|
assert.strictEqual(cycle(new SubError('foo')).name, 'Error');
|
||||||
|
|
||||||
|
assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' });
|
||||||
|
assert.strictEqual(cycle(Function), '[Function: Function]');
|
||||||
|
|
||||||
|
{
|
||||||
|
const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42);
|
||||||
|
assert(/^TypeError \[ERR_INVALID_ARG_TYPE\]:/.test(err));
|
||||||
|
assert.strictEqual(err.name, 'TypeError [ERR_INVALID_ARG_TYPE]');
|
||||||
|
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
|
||||||
|
}
|
@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
|
|||||||
const w = new Worker(__filename);
|
const w = new Worker(__filename);
|
||||||
w.on('message', common.mustNotCall());
|
w.on('message', common.mustNotCall());
|
||||||
w.on('error', common.mustCall((err) => {
|
w.on('error', common.mustCall((err) => {
|
||||||
// TODO(addaleax): be more specific here
|
assert(/^Error: foo$/.test(err));
|
||||||
assert(/foo/.test(err));
|
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
|
@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER) {
|
|||||||
const w = new Worker(__filename);
|
const w = new Worker(__filename);
|
||||||
w.on('message', common.mustNotCall());
|
w.on('message', common.mustNotCall());
|
||||||
w.on('error', common.mustCall((err) => {
|
w.on('error', common.mustCall((err) => {
|
||||||
// TODO(addaleax): be more specific here
|
assert(/^Error: foo$/.test(err));
|
||||||
assert(/foo/.test(err));
|
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
throw new Error('foo');
|
throw new Error('foo');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user