From b29491fd301c97d546da4f5b5c964e649f3e6775 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Mon, 16 Feb 2026 20:46:02 +0100 Subject: [PATCH 1/3] lib,src: preserve domexception in v8 serialize/deserialize --- lib/v8.js | 25 +++++++++++++++++ src/node_serdes.cc | 23 ++++++++++++++++ test/parallel/test-v8-serdes-domexception.js | 29 ++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 test/parallel/test-v8-serdes-domexception.js 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..eda38e4e0b1e95 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,27 @@ 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..62c95a0e4eead0 --- /dev/null +++ b/test/parallel/test-v8-serdes-domexception.js @@ -0,0 +1,29 @@ +'use strict'; + +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); +} From 32f93848b8c01ce81f87514b1325a6d27a040b59 Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Mon, 16 Feb 2026 21:06:18 +0100 Subject: [PATCH 2/3] lib,src: lint and fix test --- src/node_serdes.cc | 4 +++- test/parallel/test-v8-serdes-domexception.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/node_serdes.cc b/src/node_serdes.cc index eda38e4e0b1e95..5b3fbe0848a6cd 100644 --- a/src/node_serdes.cc +++ b/src/node_serdes.cc @@ -161,7 +161,9 @@ Maybe SerializerContext::IsHostObject(Isolate* isolate, } bool is_domexception = false; - if (!object->InstanceOf(env()->context(), domexception_ctor_val.As()) + if (!object + ->InstanceOf(env()->context(), + domexception_ctor_val.As()) .To(&is_domexception)) { return Nothing(); } diff --git a/test/parallel/test-v8-serdes-domexception.js b/test/parallel/test-v8-serdes-domexception.js index 62c95a0e4eead0..233e6511611118 100644 --- a/test/parallel/test-v8-serdes-domexception.js +++ b/test/parallel/test-v8-serdes-domexception.js @@ -1,5 +1,6 @@ 'use strict'; +require('../common'); const assert = require('node:assert'); const { serialize, deserialize } = require('node:v8'); From b033bf488aed50001f86fa290b73524c222acdbc Mon Sep 17 00:00:00 2001 From: Efe Karasakal Date: Mon, 16 Feb 2026 21:12:10 +0100 Subject: [PATCH 3/3] lib,src: format-cpp --- src/node_serdes.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node_serdes.cc b/src/node_serdes.cc index 5b3fbe0848a6cd..d9e192fd78ba66 100644 --- a/src/node_serdes.cc +++ b/src/node_serdes.cc @@ -162,8 +162,7 @@ Maybe SerializerContext::IsHostObject(Isolate* isolate, bool is_domexception = false; if (!object - ->InstanceOf(env()->context(), - domexception_ctor_val.As()) + ->InstanceOf(env()->context(), domexception_ctor_val.As()) .To(&is_domexception)) { return Nothing(); }