Skip to content
This repository has been archived by the owner on Aug 31, 2018. It is now read-only.

Commit

Permalink
worker: improve error (de)serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax committed Oct 19, 2017
1 parent a65379d commit c2b23bb
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 13 deletions.
118 changes: 118 additions & 0 deletions 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 };
11 changes: 2 additions & 9 deletions lib/internal/worker.js
@@ -1,12 +1,13 @@
'use strict';

const Buffer = require('buffer').Buffer;
const EventEmitter = require('events');
const assert = require('assert');
const path = require('path');
const util = require('util');
const errors = require('internal/errors');

const { serializeError, deserializeError } = require('internal/error-serdes');

const { MessagePort, MessageChannel } = process.binding('messaging');
util.inherits(MessagePort, EventEmitter);

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Expand Up @@ -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',
Expand Down
46 changes: 46 additions & 0 deletions 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');
}
3 changes: 1 addition & 2 deletions test/parallel/test-worker-uncaught-exception-async.js
Expand Up @@ -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(() => {
Expand Down
3 changes: 1 addition & 2 deletions test/parallel/test-worker-uncaught-exception.js
Expand Up @@ -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');
Expand Down

0 comments on commit c2b23bb

Please sign in to comment.