diff --git a/lib/internal/error-serdes.js b/lib/internal/error-serdes.js new file mode 100644 index 0000000000..afedc3ae58 --- /dev/null +++ b/lib/internal/error-serdes.js @@ -0,0 +1,118 @@ +'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 (e) {} + } + 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 (e) {} + const serialized = serialize({ + constructor: name, + properties: TryGetAllProperties(error) + }); + return Buffer.concat([Buffer.from([kSerializedError]), serialized]); + } + } + } + } catch (e) {} + try { + const serialized = serialize(error); + return Buffer.concat([Buffer.from([kSerializedObject]), serialized]); + } catch (e) {} + 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.slice(1)); + const ctor = errors[constructor]; + return Object.create(ctor.prototype, properties); + case kSerializedObject: + return deserialize(error.slice(1)); + case kInspectedError: + return error.toString('utf8', 1); + } + require('assert').fail('This should not happen'); +} + +module.exports = { serializeError, deserializeError }; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index c8c4661a0f..2ea824b6c1 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -1,6 +1,5 @@ 'use strict'; -const Buffer = require('buffer').Buffer; const EventEmitter = require('events'); const assert = require('assert'); const path = require('path'); @@ -8,6 +7,8 @@ const util = require('util'); const errors = require('internal/errors'); const { MessagePort, MessageChannel } = internalBinding('messaging'); +const { serializeError, deserializeError } = require('internal/error-serdes'); + util.inherits(MessagePort, EventEmitter); const { @@ -220,14 +221,6 @@ function setupChild(evalScript) { port.start(); } -// TODO(addaleax): These can be improved a lot. -function serializeError(error) { - return Buffer.from(util.inspect(error), 'utf8'); -} - -function deserializeError(error) { - return error.toString('utf8'); -} module.exports = { MessagePort, diff --git a/node.gyp b/node.gyp index e54e158ffb..b979d86d4d 100644 --- a/node.gyp +++ b/node.gyp @@ -95,6 +95,7 @@ 'lib/internal/crypto/util.js', 'lib/internal/encoding.js', 'lib/internal/errors.js', + 'lib/internal/error-serdes.js', 'lib/internal/freelist.js', 'lib/internal/fs.js', 'lib/internal/http.js', diff --git a/test/parallel/test-error-serdes.js b/test/parallel/test-error-serdes.js new file mode 100644 index 0000000000..ed49496b2b --- /dev/null +++ b/test/parallel/test-error-serdes.js @@ -0,0 +1,46 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const errors = require('internal/errors'); +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 errors.TypeError('ERR_INVALID_ARG_TYPE', 'object', 'object'); + 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'); +} diff --git a/test/parallel/test-worker-uncaught-exception-async.js b/test/parallel/test-worker-uncaught-exception-async.js index 606e2e9f22..88b1f41c86 100644 --- a/test/parallel/test-worker-uncaught-exception-async.js +++ b/test/parallel/test-worker-uncaught-exception-async.js @@ -7,8 +7,7 @@ if (isMainThread) { const w = new Worker(__filename); w.on('message', common.mustNotCall()); w.on('error', common.mustCall((err) => { - // TODO(addaleax): be more specific here - assert(/foo/.test(err)); + assert(/^Error: foo$/.test(err)); })); } else { setImmediate(() => { diff --git a/test/parallel/test-worker-uncaught-exception.js b/test/parallel/test-worker-uncaught-exception.js index 521972319c..b9a303e6ff 100644 --- a/test/parallel/test-worker-uncaught-exception.js +++ b/test/parallel/test-worker-uncaught-exception.js @@ -7,8 +7,7 @@ if (isMainThread) { const w = new Worker(__filename); w.on('message', common.mustNotCall()); w.on('error', common.mustCall((err) => { - // TODO(addaleax): be more specific here - assert(/foo/.test(err)); + assert(/^Error: foo$/.test(err)); })); } else { throw new Error('foo');