Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/internal/per_context/domexception.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, {
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down
170 changes: 169 additions & 1 deletion src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -67,6 +68,10 @@ bool Message::IsCloseMessage() const {

namespace {

MaybeLocal<Function> GetDOMException(Local<Context> 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 {
Expand All @@ -84,12 +89,63 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
wasm_modules_(wasm_modules),
shared_value_conveyor_(shared_value_conveyor) {}

MaybeLocal<Object> ReadDOMException(Isolate* isolate,
Local<Context> context,
v8::ValueDeserializer* deserializer) {
Local<Value> name, message, stack;
if (!deserializer->ReadValue(context).ToLocal(&name) ||
!deserializer->ReadValue(context).ToLocal(&message) ||
!deserializer->ReadValue(context).ToLocal(&stack)) {
return MaybeLocal<Object>();
}

bool has_code = false;
Local<Value> 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<Object> 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<Object>();
}

return placeholder;
}

MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
// Identifying the index in the message's BaseObject array is sufficient.
uint32_t id;
if (!deserializer->ReadUint32(&id))
return MaybeLocal<Object>();
if (id != kNormalObject) {
if (id != kNormalObject && id != kDOMExceptionTag) {
CHECK_LT(id, host_objects_.size());
Local<Object> object = host_objects_[id]->object(isolate);
if (env_->js_transferable_constructor_template()->HasInstance(object)) {
Expand All @@ -100,6 +156,9 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
}
EscapableHandleScope scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
if (id == kDOMExceptionTag) {
return ReadDOMException(isolate, context, deserializer);
}
Local<Value> object;
if (!deserializer->ReadValue(context).ToLocal(&object))
return MaybeLocal<Object>();
Expand Down Expand Up @@ -137,6 +196,72 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {

} // anonymous namespace

MaybeLocal<Object> ConvertDOMExceptionData(Environment* env,
Local<Value> dom_exception) {
if (!dom_exception->IsObject()) return MaybeLocal<Object>();
Local<Object> dom_exception_obj = dom_exception.As<Object>();
Local<Context> context = env->context();
Isolate* isolate = context->GetIsolate();

Local<String> marker_key =
FIXED_ONE_BYTE_STRING(isolate, "__domexception_placeholder");
Local<Value> marker_val;
if (!dom_exception_obj->Get(context, marker_key).ToLocal(&marker_val) ||
!marker_val->IsTrue()) {
return MaybeLocal<Object>();
}

Local<String> name_key =
FIXED_ONE_BYTE_STRING(isolate, "__domexception_name");
Local<String> message_key =
FIXED_ONE_BYTE_STRING(isolate, "__domexception_message");
Local<String> code_key =
FIXED_ONE_BYTE_STRING(isolate, "__domexception_code");
Local<String> stack_key =
FIXED_ONE_BYTE_STRING(isolate, "__domexception_stack");

Local<Value> 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<Object>();
}
bool has_code = dom_exception_obj->Get(context, code_key).ToLocal(&code);
Local<Function> dom_exception_ctor;
if (!GetDOMException(context).ToLocal(&dom_exception_ctor)) {
return MaybeLocal<Object>();
}

// Create arguments for the constructor according to the JS implementation
// First arg: message
// Second arg: options object with name and potentially code
Local<Object> options = Object::New(isolate);
if (options
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "name"),
name)
.IsNothing()) {
return MaybeLocal<Object>();
}

if (has_code &&
options
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "code"),
code)
.IsNothing()) {
return MaybeLocal<Object>();
}

Local<Value> argv[2] = {message, options};
Local<Object> 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<Object>();
}
return final_dom_exception;
}

MaybeLocal<Value> Message::Deserialize(Environment* env,
Local<Context> context,
Local<Value>* port_list) {
Expand Down Expand Up @@ -228,6 +353,12 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
return {};
}

Local<Object> 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);
}
Expand Down Expand Up @@ -297,6 +428,11 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
isolate->ThrowException(exception);
}

Maybe<bool> IsDOMException(Environment* env,
Local<Object> 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.
Expand All @@ -316,6 +452,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return Just(true);
}

Maybe<bool> 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));
}

Expand All @@ -331,6 +472,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return WriteHostObject(js_transferable);
}

Maybe<bool> 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() &&
Expand Down Expand Up @@ -427,6 +573,28 @@ class SerializerDelegate : public ValueSerializer::Delegate {
ValueSerializer* serializer = nullptr;

private:
Maybe<bool> WriteDOMException(Local<Context> context,
Local<Object> exception) {
serializer->WriteUint32(kDOMExceptionTag);

Local<Value> 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<bool>();
}

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<bool>();
}

return Just(true);
}
Maybe<bool> WriteHostObject(BaseObjectPtr<BaseObject> host_object) {
BaseObject::TransferMode mode = host_object->GetTransferMode();
if (mode == TransferMode::kDisallowCloneAndTransfer) {
Expand Down
32 changes: 32 additions & 0 deletions test/parallel/test-structuredClone-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
30 changes: 30 additions & 0 deletions test/parallel/test-worker-message-port-domexception.js
Original file line number Diff line number Diff line change
@@ -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 });
}