diff --git a/lib/internal/per_context/domexception.js b/lib/internal/per_context/domexception.js index cf9042fec7c341..7c0aac85387cbb 100644 --- a/lib/internal/per_context/domexception.js +++ b/lib/internal/per_context/domexception.js @@ -13,6 +13,9 @@ const { TypeError, } = primordials; +/* eslint-disable no-undef */ +const isDomException = privateSymbols?.is_dom_exception; + function throwInvalidThisError(Base, type) { const err = new Base(); const key = 'ERR_INVALID_THIS'; @@ -52,6 +55,10 @@ class DOMException { constructor(message = '', options = 'Error') { ErrorCaptureStackTrace(this); + if (isDomException) { + this[isDomException] = true; + } + if (options && typeof options === 'object') { const { name } = options; internalsMap.set(this, { diff --git a/src/env_properties.h b/src/env_properties.h index b255216d7f44f4..59ebeffdf6602c 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -23,6 +23,7 @@ V(decorated_private_symbol, "node:decorated") \ V(transfer_mode_private_symbol, "node:transfer_mode") \ V(host_defined_option_symbol, "node:host_defined_option_symbol") \ + V(is_dom_exception, "node:is_dom_exception") \ V(js_transferable_wrapper_private_symbol, "node:js_transferable_wrapper") \ V(entry_point_module_private_symbol, "node:entry_point_module") \ V(entry_point_promise_private_symbol, "node:entry_point_promise") \ diff --git a/src/node_messaging.cc b/src/node_messaging.cc index 9ddbb211ac2cc2..c708775d1b2c11 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -18,6 +18,7 @@ using v8::BackingStore; using v8::CompiledWasmModule; using v8::Context; using v8::EscapableHandleScope; +using v8::Exception; using v8::Function; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -67,6 +68,10 @@ bool Message::IsCloseMessage() const { namespace { +MaybeLocal GetDOMException(Local context); + +static const uint32_t kDOMExceptionTag = 0xD011; + // This is used to tell V8 how to read transferred host objects, like other // `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them. class DeserializerDelegate : public ValueDeserializer::Delegate { @@ -84,12 +89,63 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { wasm_modules_(wasm_modules), shared_value_conveyor_(shared_value_conveyor) {} + MaybeLocal ReadDOMException(Isolate* isolate, + Local context, + v8::ValueDeserializer* deserializer) { + Local name, message, stack; + if (!deserializer->ReadValue(context).ToLocal(&name) || + !deserializer->ReadValue(context).ToLocal(&message) || + !deserializer->ReadValue(context).ToLocal(&stack)) { + return MaybeLocal(); + } + + bool has_code = false; + Local code; + has_code = deserializer->ReadValue(context).ToLocal(&code); + + // V8 disallows executing JS code in the deserialization process, so we + // cannot create a DOMException object directly. Instead, we create a + // placeholder object that will be converted to a DOMException object + // later on. + Local placeholder = Object::New(isolate); + if (placeholder + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "__domexception_name"), + name) + .IsNothing() || + placeholder + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "__domexception_message"), + message) + .IsNothing() || + (has_code && + placeholder + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "__domexception_code"), + code) + .IsNothing()) || + placeholder + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "__domexception_stack"), + stack) + .IsNothing() || + placeholder + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "__domexception_placeholder"), + v8::True(isolate)) + .IsNothing()) { + return MaybeLocal(); + } + + return placeholder; + } + MaybeLocal ReadHostObject(Isolate* isolate) override { // Identifying the index in the message's BaseObject array is sufficient. uint32_t id; if (!deserializer->ReadUint32(&id)) return MaybeLocal(); - if (id != kNormalObject) { + if (id != kNormalObject && id != kDOMExceptionTag) { CHECK_LT(id, host_objects_.size()); Local object = host_objects_[id]->object(isolate); if (env_->js_transferable_constructor_template()->HasInstance(object)) { @@ -100,6 +156,9 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { } EscapableHandleScope scope(isolate); Local context = isolate->GetCurrentContext(); + if (id == kDOMExceptionTag) { + return ReadDOMException(isolate, context, deserializer); + } Local object; if (!deserializer->ReadValue(context).ToLocal(&object)) return MaybeLocal(); @@ -137,6 +196,72 @@ class DeserializerDelegate : public ValueDeserializer::Delegate { } // anonymous namespace +MaybeLocal ConvertDOMExceptionData(Environment* env, + Local dom_exception) { + if (!dom_exception->IsObject()) return MaybeLocal(); + Local dom_exception_obj = dom_exception.As(); + Local context = env->context(); + Isolate* isolate = context->GetIsolate(); + + Local marker_key = + FIXED_ONE_BYTE_STRING(isolate, "__domexception_placeholder"); + Local marker_val; + if (!dom_exception_obj->Get(context, marker_key).ToLocal(&marker_val) || + !marker_val->IsTrue()) { + return MaybeLocal(); + } + + Local name_key = + FIXED_ONE_BYTE_STRING(isolate, "__domexception_name"); + Local message_key = + FIXED_ONE_BYTE_STRING(isolate, "__domexception_message"); + Local code_key = + FIXED_ONE_BYTE_STRING(isolate, "__domexception_code"); + Local stack_key = + FIXED_ONE_BYTE_STRING(isolate, "__domexception_stack"); + + Local name, message, code, stack; + if (!dom_exception_obj->Get(context, name_key).ToLocal(&name) || + !dom_exception_obj->Get(context, message_key).ToLocal(&message) || + !dom_exception_obj->Get(context, stack_key).ToLocal(&stack)) { + return MaybeLocal(); + } + bool has_code = dom_exception_obj->Get(context, code_key).ToLocal(&code); + Local dom_exception_ctor; + if (!GetDOMException(context).ToLocal(&dom_exception_ctor)) { + return MaybeLocal(); + } + + // Create arguments for the constructor according to the JS implementation + // First arg: message + // Second arg: options object with name and potentially code + Local options = Object::New(isolate); + if (options + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "name"), + name) + .IsNothing()) { + return MaybeLocal(); + } + + if (has_code && + options + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "code"), + code) + .IsNothing()) { + return MaybeLocal(); + } + + Local argv[2] = {message, options}; + Local final_dom_exception; + if (!dom_exception_ctor->NewInstance(context, 2, argv).ToLocal(&final_dom_exception) || + !final_dom_exception->Set(context, env->stack_string(), stack).IsJust()) { + return MaybeLocal(); + } + return final_dom_exception; +} + MaybeLocal Message::Deserialize(Environment* env, Local context, Local* port_list) { @@ -228,6 +353,12 @@ MaybeLocal Message::Deserialize(Environment* env, return {}; } + Local converted_dom_exception; + if (ConvertDOMExceptionData(env, return_value) + .ToLocal(&converted_dom_exception)) { + return handle_scope.Escape(converted_dom_exception); + } + host_objects.clear(); return handle_scope.Escape(return_value); } @@ -297,6 +428,11 @@ void ThrowDataCloneException(Local context, Local message) { isolate->ThrowException(exception); } +Maybe IsDOMException(Environment* env, + Local obj) { + return obj->HasPrivate(env->context(), env->is_dom_exception()); +} + // This tells V8 how to serialize objects that it does not understand // (e.g. C++ objects) into the output buffer, in a way that our own // DeserializerDelegate understands how to unpack. @@ -316,6 +452,11 @@ class SerializerDelegate : public ValueSerializer::Delegate { return Just(true); } + Maybe is_dom_exception = IsDOMException(env_, object); + if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) { + return Just(true); + } + return Just(JSTransferable::IsJSTransferable(env_, context_, object)); } @@ -331,6 +472,11 @@ class SerializerDelegate : public ValueSerializer::Delegate { return WriteHostObject(js_transferable); } + Maybe is_dom_exception = IsDOMException(env_, object); + if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) { + return WriteDOMException(context_, object); + } + // Convert process.env to a regular object. auto env_proxy_ctor_template = env_->env_proxy_ctor_template(); if (!env_proxy_ctor_template.IsEmpty() && @@ -427,6 +573,28 @@ class SerializerDelegate : public ValueSerializer::Delegate { ValueSerializer* serializer = nullptr; private: + Maybe WriteDOMException(Local context, + Local exception) { + serializer->WriteUint32(kDOMExceptionTag); + + Local name_val, message_val, code_val, stack_val; + if (!exception->Get(context, env_->name_string()).ToLocal(&name_val) || + !exception->Get(context, env_->message_string()) + .ToLocal(&message_val) || + !exception->Get(context, env_->stack_string()).ToLocal(&stack_val) || + !exception->Get(context, env_->code_string()).ToLocal(&code_val)) { + return Nothing(); + } + + if (serializer->WriteValue(context, name_val).IsNothing() || + serializer->WriteValue(context, message_val).IsNothing() || + serializer->WriteValue(context, code_val).IsNothing() || + serializer->WriteValue(context, stack_val).IsNothing()) { + return Nothing(); + } + + return Just(true); + } Maybe WriteHostObject(BaseObjectPtr host_object) { BaseObject::TransferMode mode = host_object->GetTransferMode(); if (mode == TransferMode::kDisallowCloneAndTransfer) { diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index e6b63c382b39b1..7455edf1b0a1f2 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -86,5 +86,37 @@ for (const Transferrable of [File, Blob]) { assert.deepStrictEqual(cloned, {}); } +{ + const [e, c] = (() => { + try { + structuredClone(() => {}); + } catch (e) { + return [e, structuredClone(e)]; + } + })(); + + assert.strictEqual(e instanceof Error, c instanceof Error); + assert.strictEqual(e.name, c.name); + assert.strictEqual(e.message, c.message); + assert.strictEqual(e.code, c.code); +} + +{ + const domexception = new DOMException('test'); + const clone = structuredClone(domexception); + const clone2 = structuredClone(clone); + assert.strictEqual(clone2 instanceof DOMException, true); + assert.strictEqual(clone2.message, domexception.message); + assert.strictEqual(clone2.name, domexception.name); + assert.strictEqual(clone2.code, domexception.code); +} + +{ + const obj = {}; + Object.setPrototypeOf(obj, DOMException.prototype); + const clone = structuredClone(obj); + assert.strictEqual(clone instanceof DOMException, false); +} + const blob = new Blob(); assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' }); diff --git a/test/parallel/test-worker-message-port-domexception.js b/test/parallel/test-worker-message-port-domexception.js new file mode 100644 index 00000000000000..a14b4368298f6e --- /dev/null +++ b/test/parallel/test-worker-message-port-domexception.js @@ -0,0 +1,30 @@ +// Flags: --expose-gc +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { Worker } = require('worker_threads'); + +{ + const domexception = new DOMException('test'); + const w = new Worker(` + const { parentPort } = require('worker_threads'); + parentPort.on('message', (event) => { + if (event.type === 'error') { + console.log('event', event.domexception); + parentPort.postMessage(event); + } + }); + `, { eval: true }); + + + w.on('message', common.mustCall(({ domexception: d }) => { + assert.strictEqual(d.message, domexception.message); + assert.strictEqual(d.name, domexception.name); + assert.strictEqual(d.code, domexception.code); + globalThis.gc(); + w.terminate(); + })); + + w.postMessage({ type: 'error', domexception }); +}