From 868e0fb2869ff3676b09196d4ffb879a03be17e6 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Mon, 8 May 2023 21:28:33 +0300 Subject: [PATCH] worker: support more cases when (de)serializing errors - error.cause is potentially an error, so is now handled recursively - best effort to serialize thrown symbols - handle thrown object with custom inspect --- lib/internal/error_serdes.js | 52 ++++++++++++++++++++++++++---- test/parallel/test-error-serdes.js | 23 +++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/lib/internal/error_serdes.js b/lib/internal/error_serdes.js index 13f3f8b35fdab0..28ff80330ddb98 100644 --- a/lib/internal/error_serdes.js +++ b/lib/internal/error_serdes.js @@ -13,19 +13,24 @@ const { ObjectGetOwnPropertyNames, ObjectGetPrototypeOf, ObjectKeys, + ObjectPrototypeHasOwnProperty, ObjectPrototypeToString, RangeError, ReferenceError, SafeSet, SymbolToStringTag, SyntaxError, + SymbolFor, TypeError, URIError, } = primordials; +const { inspect: { custom: customInspectSymbol } } = require('util'); const kSerializedError = 0; const kSerializedObject = 1; const kInspectedError = 2; +const kInspectedSymbol = 3; +const kCustomInspectedObject = 4; const errors = { Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError, @@ -52,7 +57,13 @@ function TryGetAllProperties(object, target = object) { // Continue regardless of error. } } - if ('value' in descriptor && typeof descriptor.value !== 'function') { + if (key === 'cause') { + delete descriptor.get; + delete descriptor.set; + descriptor.value = serializeError(descriptor.value); + all[key] = descriptor; + } else if ('value' in descriptor && + typeof descriptor.value !== 'function' && typeof descriptor.value !== 'symbol') { delete descriptor.get; delete descriptor.set; all[key] = descriptor; @@ -95,6 +106,10 @@ function inspect(...args) { let serialize; function serializeError(error) { if (!serialize) serialize = require('v8').serialize; + if (typeof error === 'symbol') { + return Buffer.concat([Buffer.from([kInspectedSymbol]), + Buffer.from(inspect(error), 'utf8')]); + } try { if (typeof error === 'object' && ObjectPrototypeToString(error) === '[object Error]') { @@ -113,6 +128,15 @@ function serializeError(error) { } catch { // Continue regardless of error. } + try { + if (error != null && + ObjectPrototypeHasOwnProperty(error, customInspectSymbol)) { + return Buffer.concat([Buffer.from([kCustomInspectedObject]), + Buffer.from(inspect(error), 'utf8')]); + } + } catch { + // Continue regardless of error. + } try { const serialized = serialize(error); return Buffer.concat([Buffer.from([kSerializedObject]), serialized]); @@ -123,6 +147,12 @@ function serializeError(error) { Buffer.from(inspect(error), 'utf8')]); } +function fromBuffer(error) { + return Buffer.from(error.buffer, + error.byteOffset + 1, + error.byteLength - 1); +} + let deserialize; function deserializeError(error) { if (!deserialize) deserialize = require('v8').deserialize; @@ -132,19 +162,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(buf.toString('utf8').substring('Symbol('.length, 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..95c57dc7062f5a 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,16 @@ assert.strictEqual(cycle(new SubError('foo')).name, 'Error'); assert.deepStrictEqual(cycle({ message: 'foo' }), { message: 'foo' }); assert.strictEqual(cycle(Function), '[Function: Function]'); + +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(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')); + + { const err = new ERR_INVALID_ARG_TYPE('object', 'Object', 42); assert.match(String(err), /^TypeError \[ERR_INVALID_ARG_TYPE\]:/); @@ -66,3 +80,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');