Skip to content

Commit

Permalink
src: support (de)serialization of DOMException
Browse files Browse the repository at this point in the history
  • Loading branch information
jazelly committed Mar 8, 2025
1 parent 3a497dc commit 0aec3cf
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 3 deletions.
194 changes: 191 additions & 3 deletions src/node_messaging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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 @@ -83,11 +87,66 @@ 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;
if (!deserializer->ReadValue(context).ToLocal(&name) ||
!deserializer->ReadValue(context).ToLocal(&message)) {
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,
String::NewFromUtf8(isolate, "__domexception_name")
.ToLocalChecked(),
name)
.IsNothing() ||
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_message")
.ToLocalChecked(),
message)
.IsNothing() ||
(has_code &&
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_code")
.ToLocalChecked(),
code)
.IsNothing()) ||
placeholder
->Set(context,
String::NewFromUtf8(isolate, "__domexception_placeholder")
.ToLocalChecked(),
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>();

Local<Context> context = isolate->GetCurrentContext();
if (id == kDOMExceptionTag) {
return ReadDOMException(isolate, context, deserializer);
}

if (id != kNormalObject) {
CHECK_LT(id, host_objects_.size());
Local<Object> object = host_objects_[id]->object(isolate);
Expand All @@ -98,7 +157,6 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
}
}
EscapableHandleScope scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Value> object;
if (!deserializer->ReadValue(context).ToLocal(&object))
return MaybeLocal<Object>();
Expand Down Expand Up @@ -136,6 +194,71 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {

} // anonymous namespace

MaybeLocal<Object> ConvertDOMExceptionData(Local<Context> context,
Local<Value> value) {
if (!value->IsObject()) return MaybeLocal<Object>();

Isolate* isolate = context->GetIsolate();
Local<Object> obj = value.As<Object>();

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

Local<String> name_key =
String::NewFromUtf8(isolate, "__domexception_name").ToLocalChecked();
Local<String> message_key =
String::NewFromUtf8(isolate, "__domexception_message").ToLocalChecked();
Local<String> code_key =
String::NewFromUtf8(isolate, "__domexception_code").ToLocalChecked();

Local<Value> name, message, code;
if (!obj->Get(context, name_key).ToLocal(&name) ||
!obj->Get(context, message_key).ToLocal(&message)) {
return MaybeLocal<Object>();
}
bool has_code = 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,
String::NewFromUtf8(isolate, "name").ToLocalChecked(),
name)
.IsNothing()) {
return MaybeLocal<Object>();
}

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

Local<Value> argv[2] = {message, options};
Local<Value> exception;
if (!dom_exception_ctor->NewInstance(context, 2, argv).ToLocal(&exception)) {
return MaybeLocal<Object>();
}

return exception.As<Object>();
}

MaybeLocal<Value> Message::Deserialize(Environment* env,
Local<Context> context,
Local<Value>* port_list) {
Expand Down Expand Up @@ -227,8 +350,14 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
return {};
}

host_objects.clear();
return handle_scope.Escape(return_value);
Local<Object> converted_dom_exception;
if (!ConvertDOMExceptionData(context, return_value)
.ToLocal(&converted_dom_exception)) {
host_objects.clear();
return handle_scope.Escape(return_value);
}

return handle_scope.Escape(converted_dom_exception);
}

void Message::AddSharedArrayBuffer(
Expand Down Expand Up @@ -294,6 +423,37 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
isolate->ThrowException(exception);
}

Maybe<bool> IsDOMException(Isolate* isolate,
Local<Context> context,
Local<Object> obj) {
HandleScope handle_scope(isolate);

Local<Object> per_context_bindings;
Local<Value> dom_exception_ctor_val;

if (!GetPerContextExports(context).ToLocal(&per_context_bindings)) {
return Nothing<bool>();
}

if (!per_context_bindings
->Get(context,
String::NewFromUtf8(isolate, "DOMException").ToLocalChecked())
.ToLocal(&dom_exception_ctor_val) ||
!dom_exception_ctor_val->IsFunction()) {
return Nothing<bool>();
}

Local<Function> dom_exception_ctor = dom_exception_ctor_val.As<Function>();

Maybe<bool> result = obj->InstanceOf(context, dom_exception_ctor);

if (result.IsNothing()) {
return Nothing<bool>();
}

return Just(result.FromJust());
}

// 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 @@ -313,6 +473,9 @@ class SerializerDelegate : public ValueSerializer::Delegate {
return Just(true);
}

Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) return Just(true);

return Just(JSTransferable::IsJSTransferable(env_, context_, object));
}

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

Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, 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 @@ -424,6 +592,26 @@ 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;
if (!exception->Get(context, env_->name_string()).ToLocal(&name_val) ||
!exception->Get(context, env_->message_string())
.ToLocal(&message_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()) {
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
16 changes: 16 additions & 0 deletions test/parallel/test-structuredClone-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,21 @@ for (const Transferrable of [File, Blob]) {
assert.deepStrictEqual(cloned, {});
}

{
// https://github.com/nodejs/node/issues/49181
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 blob = new Blob();
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });

0 comments on commit 0aec3cf

Please sign in to comment.