Skip to content

Commit 0aec3cf

Browse files
committed
src: support (de)serialization of DOMException
1 parent 3a497dc commit 0aec3cf

File tree

2 files changed

+207
-3
lines changed

2 files changed

+207
-3
lines changed

src/node_messaging.cc

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ bool Message::IsCloseMessage() const {
6666

6767
namespace {
6868

69+
MaybeLocal<Function> GetDOMException(Local<Context> context);
70+
71+
static const uint32_t kDOMExceptionTag = 0xD011;
72+
6973
// This is used to tell V8 how to read transferred host objects, like other
7074
// `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them.
7175
class DeserializerDelegate : public ValueDeserializer::Delegate {
@@ -83,11 +87,66 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
8387
wasm_modules_(wasm_modules),
8488
shared_value_conveyor_(shared_value_conveyor) {}
8589

90+
MaybeLocal<Object> ReadDOMException(Isolate* isolate,
91+
Local<Context> context,
92+
v8::ValueDeserializer* deserializer) {
93+
Local<Value> name, message;
94+
if (!deserializer->ReadValue(context).ToLocal(&name) ||
95+
!deserializer->ReadValue(context).ToLocal(&message)) {
96+
return MaybeLocal<Object>();
97+
}
98+
99+
bool has_code = false;
100+
Local<Value> code;
101+
has_code = deserializer->ReadValue(context).ToLocal(&code);
102+
103+
// V8 disallows executing JS code in the deserialization process, so we
104+
// cannot create a DOMException object directly. Instead, we create a
105+
// placeholder object that will be converted to a DOMException object
106+
// later on.
107+
Local<Object> placeholder = Object::New(isolate);
108+
if (placeholder
109+
->Set(context,
110+
String::NewFromUtf8(isolate, "__domexception_name")
111+
.ToLocalChecked(),
112+
name)
113+
.IsNothing() ||
114+
placeholder
115+
->Set(context,
116+
String::NewFromUtf8(isolate, "__domexception_message")
117+
.ToLocalChecked(),
118+
message)
119+
.IsNothing() ||
120+
(has_code &&
121+
placeholder
122+
->Set(context,
123+
String::NewFromUtf8(isolate, "__domexception_code")
124+
.ToLocalChecked(),
125+
code)
126+
.IsNothing()) ||
127+
placeholder
128+
->Set(context,
129+
String::NewFromUtf8(isolate, "__domexception_placeholder")
130+
.ToLocalChecked(),
131+
v8::True(isolate))
132+
.IsNothing()) {
133+
return MaybeLocal<Object>();
134+
}
135+
136+
return placeholder;
137+
}
138+
86139
MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
87140
// Identifying the index in the message's BaseObject array is sufficient.
88141
uint32_t id;
89142
if (!deserializer->ReadUint32(&id))
90143
return MaybeLocal<Object>();
144+
145+
Local<Context> context = isolate->GetCurrentContext();
146+
if (id == kDOMExceptionTag) {
147+
return ReadDOMException(isolate, context, deserializer);
148+
}
149+
91150
if (id != kNormalObject) {
92151
CHECK_LT(id, host_objects_.size());
93152
Local<Object> object = host_objects_[id]->object(isolate);
@@ -98,7 +157,6 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
98157
}
99158
}
100159
EscapableHandleScope scope(isolate);
101-
Local<Context> context = isolate->GetCurrentContext();
102160
Local<Value> object;
103161
if (!deserializer->ReadValue(context).ToLocal(&object))
104162
return MaybeLocal<Object>();
@@ -136,6 +194,71 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
136194

137195
} // anonymous namespace
138196

197+
MaybeLocal<Object> ConvertDOMExceptionData(Local<Context> context,
198+
Local<Value> value) {
199+
if (!value->IsObject()) return MaybeLocal<Object>();
200+
201+
Isolate* isolate = context->GetIsolate();
202+
Local<Object> obj = value.As<Object>();
203+
204+
Local<String> marker_key =
205+
String::NewFromUtf8(isolate, "__domexception_placeholder")
206+
.ToLocalChecked();
207+
Local<Value> marker_val;
208+
if (!obj->Get(context, marker_key).ToLocal(&marker_val) ||
209+
!marker_val->IsTrue()) {
210+
return MaybeLocal<Object>();
211+
}
212+
213+
Local<String> name_key =
214+
String::NewFromUtf8(isolate, "__domexception_name").ToLocalChecked();
215+
Local<String> message_key =
216+
String::NewFromUtf8(isolate, "__domexception_message").ToLocalChecked();
217+
Local<String> code_key =
218+
String::NewFromUtf8(isolate, "__domexception_code").ToLocalChecked();
219+
220+
Local<Value> name, message, code;
221+
if (!obj->Get(context, name_key).ToLocal(&name) ||
222+
!obj->Get(context, message_key).ToLocal(&message)) {
223+
return MaybeLocal<Object>();
224+
}
225+
bool has_code = obj->Get(context, code_key).ToLocal(&code);
226+
227+
Local<Function> dom_exception_ctor;
228+
if (!GetDOMException(context).ToLocal(&dom_exception_ctor)) {
229+
return MaybeLocal<Object>();
230+
}
231+
232+
// Create arguments for the constructor according to the JS implementation
233+
// First arg: message
234+
// Second arg: options object with name and potentially code
235+
Local<Object> options = Object::New(isolate);
236+
if (options
237+
->Set(context,
238+
String::NewFromUtf8(isolate, "name").ToLocalChecked(),
239+
name)
240+
.IsNothing()) {
241+
return MaybeLocal<Object>();
242+
}
243+
244+
if (has_code &&
245+
options
246+
->Set(context,
247+
String::NewFromUtf8(isolate, "code").ToLocalChecked(),
248+
code)
249+
.IsNothing()) {
250+
return MaybeLocal<Object>();
251+
}
252+
253+
Local<Value> argv[2] = {message, options};
254+
Local<Value> exception;
255+
if (!dom_exception_ctor->NewInstance(context, 2, argv).ToLocal(&exception)) {
256+
return MaybeLocal<Object>();
257+
}
258+
259+
return exception.As<Object>();
260+
}
261+
139262
MaybeLocal<Value> Message::Deserialize(Environment* env,
140263
Local<Context> context,
141264
Local<Value>* port_list) {
@@ -227,8 +350,14 @@ MaybeLocal<Value> Message::Deserialize(Environment* env,
227350
return {};
228351
}
229352

230-
host_objects.clear();
231-
return handle_scope.Escape(return_value);
353+
Local<Object> converted_dom_exception;
354+
if (!ConvertDOMExceptionData(context, return_value)
355+
.ToLocal(&converted_dom_exception)) {
356+
host_objects.clear();
357+
return handle_scope.Escape(return_value);
358+
}
359+
360+
return handle_scope.Escape(converted_dom_exception);
232361
}
233362

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

426+
Maybe<bool> IsDOMException(Isolate* isolate,
427+
Local<Context> context,
428+
Local<Object> obj) {
429+
HandleScope handle_scope(isolate);
430+
431+
Local<Object> per_context_bindings;
432+
Local<Value> dom_exception_ctor_val;
433+
434+
if (!GetPerContextExports(context).ToLocal(&per_context_bindings)) {
435+
return Nothing<bool>();
436+
}
437+
438+
if (!per_context_bindings
439+
->Get(context,
440+
String::NewFromUtf8(isolate, "DOMException").ToLocalChecked())
441+
.ToLocal(&dom_exception_ctor_val) ||
442+
!dom_exception_ctor_val->IsFunction()) {
443+
return Nothing<bool>();
444+
}
445+
446+
Local<Function> dom_exception_ctor = dom_exception_ctor_val.As<Function>();
447+
448+
Maybe<bool> result = obj->InstanceOf(context, dom_exception_ctor);
449+
450+
if (result.IsNothing()) {
451+
return Nothing<bool>();
452+
}
453+
454+
return Just(result.FromJust());
455+
}
456+
297457
// This tells V8 how to serialize objects that it does not understand
298458
// (e.g. C++ objects) into the output buffer, in a way that our own
299459
// DeserializerDelegate understands how to unpack.
@@ -313,6 +473,9 @@ class SerializerDelegate : public ValueSerializer::Delegate {
313473
return Just(true);
314474
}
315475

476+
Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
477+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) return Just(true);
478+
316479
return Just(JSTransferable::IsJSTransferable(env_, context_, object));
317480
}
318481

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

494+
Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
495+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
496+
return WriteDOMException(context_, object);
497+
}
498+
331499
// Convert process.env to a regular object.
332500
auto env_proxy_ctor_template = env_->env_proxy_ctor_template();
333501
if (!env_proxy_ctor_template.IsEmpty() &&
@@ -424,6 +592,26 @@ class SerializerDelegate : public ValueSerializer::Delegate {
424592
ValueSerializer* serializer = nullptr;
425593

426594
private:
595+
Maybe<bool> WriteDOMException(Local<Context> context,
596+
Local<Object> exception) {
597+
serializer->WriteUint32(kDOMExceptionTag);
598+
599+
Local<Value> name_val, message_val, code_val;
600+
if (!exception->Get(context, env_->name_string()).ToLocal(&name_val) ||
601+
!exception->Get(context, env_->message_string())
602+
.ToLocal(&message_val) ||
603+
!exception->Get(context, env_->code_string()).ToLocal(&code_val)) {
604+
return Nothing<bool>();
605+
}
606+
607+
if (serializer->WriteValue(context, name_val).IsNothing() ||
608+
serializer->WriteValue(context, message_val).IsNothing() ||
609+
serializer->WriteValue(context, code_val).IsNothing()) {
610+
return Nothing<bool>();
611+
}
612+
613+
return Just(true);
614+
}
427615
Maybe<bool> WriteHostObject(BaseObjectPtr<BaseObject> host_object) {
428616
BaseObject::TransferMode mode = host_object->GetTransferMode();
429617
if (mode == TransferMode::kDisallowCloneAndTransfer) {

test/parallel/test-structuredClone-global.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,21 @@ for (const Transferrable of [File, Blob]) {
8686
assert.deepStrictEqual(cloned, {});
8787
}
8888

89+
{
90+
// https://github.com/nodejs/node/issues/49181
91+
const [e, c] = (() => {
92+
try {
93+
structuredClone(() => {});
94+
} catch (e) {
95+
return [e, structuredClone(e)];
96+
}
97+
})();
98+
99+
assert.strictEqual(e instanceof Error, c instanceof Error);
100+
assert.strictEqual(e.name, c.name);
101+
assert.strictEqual(e.message, c.message);
102+
assert.strictEqual(e.code, c.code);
103+
}
104+
89105
const blob = new Blob();
90106
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });

0 commit comments

Comments
 (0)