Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

worker: support more cases when (de)serializing errors #47925

Merged
merged 6 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
MoLow marked this conversation as resolved.
Show resolved Hide resolved
} 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;
MoLow marked this conversation as resolved.
Show resolved Hide resolved
}
});
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
54 changes: 54 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,47 @@ 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('err');
}
}

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);
MoLow marked this conversation as resolved.
Show resolved Hide resolved
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(Object.defineProperty(new Error('Error with cause'), 'cause', { get() { return { foo: 'bar' }; } })).cause,
{ foo: 'bar' }
);
MoLow marked this conversation as resolved.
Show resolved Hide resolved
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 +111,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');