Skip to content

Commit

Permalink
worker: support more cases when (de)serializing errors
Browse files Browse the repository at this point in the history
- 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>
  • Loading branch information
MoLow committed May 12, 2023
1 parent 7812327 commit 7984af6
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 11 deletions.
63 changes: 52 additions & 11 deletions lib/internal/error_serdes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,31 @@ const {
ObjectGetOwnPropertyNames,
ObjectGetPrototypeOf,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
ObjectPrototypeToString,
RangeError,
ReferenceError,
SafeSet,
StringFromCharCode,
StringPrototypeSubstring,
SymbolToStringTag,
SyntaxError,
SymbolFor,
TypeError,
TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetByteOffset,
TypedArrayPrototypeGetByteLength,
URIError,
} = primordials;
const { inspect: { custom: customInspectSymbol } } = require('util');

const kSerializedError = 0;
const kSerializedObject = 1;
const kInspectedError = 2;
const kInspectedSymbol = 3;
const kCustomInspectedObject = 4;

const kSymbolStringLength = 'Symbol('.length;

const errors = {
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError,
Expand All @@ -42,19 +54,24 @@ function TryGetAllProperties(object, target = object) {
ArrayPrototypeForEach(keys, (key) => {
let descriptor;
try {
// TODO: create a null-prototype descriptor with needed properties only
descriptor = ObjectGetOwnPropertyDescriptor(object, key);
} catch { return; }
const getter = descriptor.get;
if (getter && key !== '__proto__') {
try {
descriptor.value = FunctionPrototypeCall(getter, target);
delete descriptor.get;
delete descriptor.set;
} catch {
// Continue regardless of error.
}
}
if ('value' in descriptor && typeof descriptor.value !== 'function') {
delete descriptor.get;
delete descriptor.set;
if (key === 'cause') {
descriptor.value = serializeError(descriptor.value);
all[key] = descriptor;
} else if ('value' in descriptor &&
typeof descriptor.value !== 'function' && typeof descriptor.value !== 'symbol') {
all[key] = descriptor;
}
});
Expand Down Expand Up @@ -95,6 +112,9 @@ function inspect(...args) {
let serialize;
function serializeError(error) {
if (!serialize) serialize = require('v8').serialize;
if (typeof error === 'symbol') {
return Buffer.from(StringFromCharCode(kInspectedSymbol) + inspect(error), 'utf8');
}
try {
if (typeof error === 'object' &&
ObjectPrototypeToString(error) === '[object Error]') {
Expand All @@ -113,14 +133,27 @@ function serializeError(error) {
} catch {
// Continue regardless of error.
}
try {
if (error != null &&
ObjectPrototypeHasOwnProperty(error, customInspectSymbol)) {
return Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), 'utf8');
}
} catch {
// Continue regardless of error.
}
try {
const serialized = serialize(error);
return Buffer.concat([Buffer.from([kSerializedObject]), serialized]);
} catch {
// Continue regardless of error.
}
return Buffer.concat([Buffer.from([kInspectedError]),
Buffer.from(inspect(error), 'utf8')]);
return Buffer.from(StringFromCharCode(kInspectedError) + inspect(error), 'utf8');
}

function fromBuffer(error) {
return Buffer.from(TypedArrayPrototypeGetBuffer(error),
TypedArrayPrototypeGetByteOffset(error) + 1,
TypedArrayPrototypeGetByteLength(error) - 1);
}

let deserialize;
Expand All @@ -132,19 +165,27 @@ function deserializeError(error) {
const ctor = errors[constructor];
ObjectDefineProperty(properties, SymbolToStringTag, {
__proto__: null,
value: { value: 'Error', configurable: true },
value: { __proto__: null, value: 'Error', configurable: true },
enumerable: true,
});
if ('cause' in properties && 'value' in properties.cause) {
properties.cause.value = deserializeError(properties.cause.value);
}
return ObjectCreate(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');
case kInspectedError:
return fromBuffer(error).toString('utf8');
case kInspectedSymbol: {
const buf = fromBuffer(error);
return SymbolFor(StringPrototypeSubstring(buf.toString('utf8'), kSymbolStringLength, buf.length - 1));
}
case kCustomInspectedObject:
return {
__proto__: null,
[customInspectSymbol]: () => fromBuffer(error).toString('utf8'),
};
}
require('assert').fail('This should not happen');
}
Expand Down
59 changes: 59 additions & 0 deletions test/parallel/test-error-serdes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
'use strict';
require('../common');
const assert = require('assert');
const { inspect } = require('util');
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { serializeError, deserializeError } = require('internal/error_serdes');

Expand All @@ -15,6 +16,9 @@ assert.strictEqual(cycle(1.4), 1.4);
assert.strictEqual(cycle(null), null);
assert.strictEqual(cycle(undefined), undefined);
assert.strictEqual(cycle('foo'), 'foo');
assert.strictEqual(cycle(Symbol.for('foo')), Symbol.for('foo'));
assert.strictEqual(cycle(Symbol('foo')).toString(), Symbol('foo').toString());


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

class ErrorWithCause extends Error {
get cause() {
return new Error('err');
}
}
class ErrorWithThowingCause extends Error {
get cause() {
throw new Error('err');
}
}
class ErrorWithCyclicCause extends Error {
get cause() {
return new ErrorWithCyclicCause();
}
}
const errorWithCause = Object
.defineProperty(new Error('Error with cause'), 'cause', { get() { return { foo: 'bar' }; } });
const errorWithThrowingCause = Object
.defineProperty(new Error('Error with cause'), 'cause', { get() { throw new Error('err'); } });
const errorWithCyclicCause = Object
.defineProperty(new Error('Error with cause'), 'cause', { get() { return errorWithCyclicCause; } });

assert.strictEqual(cycle(new Error('Error with cause', { cause: 0 })).cause, 0);
assert.strictEqual(cycle(new Error('Error with cause', { cause: -1 })).cause, -1);
assert.strictEqual(cycle(new Error('Error with cause', { cause: 1.4 })).cause, 1.4);
assert.strictEqual(cycle(new Error('Error with cause', { cause: null })).cause, null);
assert.strictEqual(cycle(new Error('Error with cause', { cause: undefined })).cause, undefined);
assert.strictEqual(Object.hasOwn(cycle(new Error('Error with cause', { cause: undefined })), 'cause'), true);
assert.strictEqual(cycle(new Error('Error with cause', { cause: 'foo' })).cause, 'foo');
assert.deepStrictEqual(cycle(new Error('Error with cause', { cause: new Error('err') })).cause, new Error('err'));
assert.deepStrictEqual(cycle(errorWithCause).cause, { foo: 'bar' });
assert.strictEqual(Object.hasOwn(cycle(errorWithThrowingCause), 'cause'), false);
assert.strictEqual(Object.hasOwn(cycle(errorWithCyclicCause), 'cause'), true);
assert.deepStrictEqual(cycle(new ErrorWithCause('Error with cause')).cause, new Error('err'));
assert.strictEqual(cycle(new ErrorWithThowingCause('Error with cause')).cause, undefined);
assert.strictEqual(Object.hasOwn(cycle(new ErrorWithThowingCause('Error with cause')), 'cause'), false);
// When the cause is cyclic, it is serialized until Maxiumum call stack size is reached
let depth = 0;
let e = cycle(new ErrorWithCyclicCause('Error with cause'));
while (e.cause) {
e = e.cause;
depth++;
}
assert(depth > 1);


{
const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42);
assert.match(String(err), /^TypeError \[ERR_INVALID_ARG_TYPE\]:/);
Expand All @@ -66,3 +116,12 @@ assert.strictEqual(cycle(Function), '[Function: Function]');
serializeError(new DynamicError());
assert.strictEqual(called, true);
}


const data = {
foo: 'bar',
[inspect.custom]() {
return 'barbaz';
}
};
assert.strictEqual(inspect(cycle(data)), 'barbaz');

0 comments on commit 7984af6

Please sign in to comment.