diff --git a/lib/internal/error_serdes.js b/lib/internal/error_serdes.js index 13f3f8b35fdab0..e4850c63aaffb3 100644 --- a/lib/internal/error_serdes.js +++ b/lib/internal/error_serdes.js @@ -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, @@ -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; } }); @@ -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]') { @@ -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; @@ -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'); } diff --git a/test/parallel/test-error-serdes.js b/test/parallel/test-error-serdes.js index 92d0864348a831..3b61d3a4b9b34b 100644 --- a/test/parallel/test-error-serdes.js +++ b/test/parallel/test-error-serdes.js @@ -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'); @@ -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++) { @@ -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\]:/); @@ -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');