diff --git a/lib/v8.js b/lib/v8.js index c0d4074aac21d5..d2c55d50002c70 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -47,6 +47,13 @@ const { Serializer, Deserializer, } = internalBinding('serdes'); +const { + DOMException, +} = internalBinding('messaging'); +const { + messaging_clone_symbol, + messaging_deserialize_symbol, +} = internalBinding('symbols'); const { namespace: startupSnapshot, } = require('internal/v8/startup_snapshot'); @@ -381,6 +388,8 @@ function arrayBufferViewIndexToType(index) { return undefined; } +const kDOMExceptionHostObjectTag = 14; + class DefaultSerializer extends Serializer { constructor() { super(); @@ -395,6 +404,14 @@ class DefaultSerializer extends Serializer { * @returns {void} */ _writeHostObject(abView) { + // Route DOMException through the same hooks used by structuredClone + if (abView instanceof DOMException) { + const { data } = abView[messaging_clone_symbol](); + this.writeUint32(kDOMExceptionHostObjectTag); + this.writeValue(data); + return; + } + // Keep track of how to handle different ArrayBufferViews. The default // Serializer for Node does not use the V8 methods for serializing those // objects because Node's `Buffer` objects use pooled allocation in many @@ -426,6 +443,14 @@ class DefaultDeserializer extends Deserializer { */ _readHostObject() { const typeIndex = this.readUint32(); + // Route DOMException through the same hooks used by structuredClone + if (typeIndex === kDOMExceptionHostObjectTag) { + const data = this.readValue(); + const domException = new DOMException(); + domException[messaging_deserialize_symbol](data); + return domException; + } + const ctor = arrayBufferViewIndexToType(typeIndex); const byteLength = this.readUint32(); const byteOffset = this._readRawBytes(byteLength); diff --git a/src/node_serdes.cc b/src/node_serdes.cc index 00fcd4b6afccce..d9e192fd78ba66 100644 --- a/src/node_serdes.cc +++ b/src/node_serdes.cc @@ -37,6 +37,8 @@ class SerializerContext : public BaseObject, ~SerializerContext() override = default; + bool HasCustomHostObject(Isolate* isolate) override { return true; } + Maybe IsHostObject(Isolate* isolate, Local object) override; void ThrowDataCloneError(Local message) override; Maybe WriteHostObject(Isolate* isolate, Local object) override; Maybe GetSharedArrayBufferId( @@ -145,6 +147,28 @@ Maybe SerializerContext::GetSharedArrayBufferId( return id->Uint32Value(env()->context()); } +Maybe SerializerContext::IsHostObject(Isolate* isolate, + Local object) { + Local per_context_bindings; + Local domexception_ctor_val; + if (!GetPerContextExports(env()->context()).ToLocal(&per_context_bindings) || + !per_context_bindings + ->Get(env()->context(), + FIXED_ONE_BYTE_STRING(env()->isolate(), "DOMException")) + .ToLocal(&domexception_ctor_val) || + !domexception_ctor_val->IsFunction()) { + return Just(object->InternalFieldCount() != 0); + } + + bool is_domexception = false; + if (!object + ->InstanceOf(env()->context(), domexception_ctor_val.As()) + .To(&is_domexception)) { + return Nothing(); + } + return Just(is_domexception || object->InternalFieldCount() != 0); +} + Maybe SerializerContext::WriteHostObject(Isolate* isolate, Local input) { Local args[1] = { input }; diff --git a/test/parallel/test-v8-serdes-domexception.js b/test/parallel/test-v8-serdes-domexception.js new file mode 100644 index 00000000000000..233e6511611118 --- /dev/null +++ b/test/parallel/test-v8-serdes-domexception.js @@ -0,0 +1,30 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { serialize, deserialize } = require('node:v8'); + +// Simple case, should preserve message/name/stack like structuredClone +{ + const e = AbortSignal.abort().reason; + + const cloned = deserialize(serialize(e)); + + assert(cloned instanceof DOMException); + assert.strictEqual(cloned.name, e.name); + assert.strictEqual(cloned.message, e.message); + + assert.strictEqual(typeof cloned.stack, 'string'); + assert(cloned.stack.includes(e.name)); +} + +// Nested case +{ + const e = AbortSignal.abort().reason; + const obj = { e }; + + const clonedObj = deserialize(serialize(obj)); + assert(clonedObj.e instanceof DOMException); + assert.strictEqual(clonedObj.e.name, e.name); + assert.strictEqual(clonedObj.e.message, e.message); +}