Skip to content

Commit ee1dacf

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

File tree

2 files changed

+212
-4
lines changed

2 files changed

+212
-4
lines changed

src/node_messaging.cc

Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "node_external_reference.h"
1010
#include "node_process-inl.h"
1111
#include "util-inl.h"
12+
#include "util.h"
1213

1314
using node::contextify::ContextifyContext;
1415
using node::errors::TryCatchScope;
@@ -66,6 +67,10 @@ bool Message::IsCloseMessage() const {
6667

6768
namespace {
6869

70+
MaybeLocal<Function> GetDOMException(Local<Context> context);
71+
72+
static const uint32_t kDOMExceptionTag = 0xD011;
73+
6974
// This is used to tell V8 how to read transferred host objects, like other
7075
// `MessagePort`s and `SharedArrayBuffer`s, and make new JS objects out of them.
7176
class DeserializerDelegate : public ValueDeserializer::Delegate {
@@ -83,11 +88,63 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
8388
wasm_modules_(wasm_modules),
8489
shared_value_conveyor_(shared_value_conveyor) {}
8590

91+
MaybeLocal<Object> ReadDOMException(Isolate* isolate,
92+
Local<Context> context,
93+
v8::ValueDeserializer* deserializer) {
94+
Local<Value> name, message;
95+
if (!deserializer->ReadValue(context).ToLocal(&name) ||
96+
!deserializer->ReadValue(context).ToLocal(&message)) {
97+
return MaybeLocal<Object>();
98+
}
99+
100+
bool has_code = false;
101+
Local<Value> code;
102+
has_code = deserializer->ReadValue(context).ToLocal(&code);
103+
104+
// V8 disallows executing JS code in the deserialization process, so we
105+
// cannot create a DOMException object directly. Instead, we create a
106+
// placeholder object that will be converted to a DOMException object
107+
// later on.
108+
Local<Object> placeholder = Object::New(isolate);
109+
if (placeholder
110+
->Set(context,
111+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_name"),
112+
name)
113+
.IsNothing() ||
114+
placeholder
115+
->Set(context,
116+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_message"),
117+
message)
118+
.IsNothing() ||
119+
(has_code &&
120+
placeholder
121+
->Set(context,
122+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_code"),
123+
code)
124+
.IsNothing()) ||
125+
placeholder
126+
->Set(context,
127+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_placeholder"),
128+
v8::True(isolate))
129+
.IsNothing()) {
130+
return MaybeLocal<Object>();
131+
}
132+
133+
return placeholder;
134+
}
135+
86136
MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
87137
// Identifying the index in the message's BaseObject array is sufficient.
88138
uint32_t id;
89139
if (!deserializer->ReadUint32(&id))
90140
return MaybeLocal<Object>();
141+
142+
EscapableHandleScope scope(isolate);
143+
Local<Context> context = isolate->GetCurrentContext();
144+
if (id == kDOMExceptionTag) {
145+
return ReadDOMException(isolate, context, deserializer);
146+
}
147+
91148
if (id != kNormalObject) {
92149
CHECK_LT(id, host_objects_.size());
93150
Local<Object> object = host_objects_[id]->object(isolate);
@@ -97,8 +154,6 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
97154
return object;
98155
}
99156
}
100-
EscapableHandleScope scope(isolate);
101-
Local<Context> context = isolate->GetCurrentContext();
102157
Local<Value> object;
103158
if (!deserializer->ReadValue(context).ToLocal(&object))
104159
return MaybeLocal<Object>();
@@ -136,6 +191,70 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
136191

137192
} // anonymous namespace
138193

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

230-
host_objects.clear();
231-
return handle_scope.Escape(return_value);
349+
Local<Object> converted_dom_exception;
350+
if (!ConvertDOMExceptionData(context, return_value)
351+
.ToLocal(&converted_dom_exception)) {
352+
host_objects.clear();
353+
return handle_scope.Escape(return_value);
354+
}
355+
356+
return handle_scope.Escape(converted_dom_exception);
232357
}
233358

234359
void Message::AddSharedArrayBuffer(
@@ -294,6 +419,36 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
294419
isolate->ThrowException(exception);
295420
}
296421

422+
Maybe<bool> IsDOMException(Isolate* isolate,
423+
Local<Context> context,
424+
Local<Object> obj) {
425+
HandleScope handle_scope(isolate);
426+
427+
Local<Object> per_context_bindings;
428+
Local<Value> dom_exception_ctor_val;
429+
430+
if (!GetPerContextExports(context).ToLocal(&per_context_bindings)) {
431+
return Just(false);
432+
}
433+
434+
if (!per_context_bindings
435+
->Get(context,
436+
FIXED_ONE_BYTE_STRING(isolate, "DOMException"))
437+
.ToLocal(&dom_exception_ctor_val) ||
438+
!dom_exception_ctor_val->IsFunction()) {
439+
return Nothing<bool>();
440+
}
441+
442+
Local<Function> dom_exception_ctor = dom_exception_ctor_val.As<Function>();
443+
Local<Value> obj_constructor;
444+
if (!obj->Get(context, FIXED_ONE_BYTE_STRING(isolate, "constructor"))
445+
.ToLocal(&obj_constructor) ||
446+
!obj_constructor->IsFunction()) {
447+
return Just(false);
448+
}
449+
return Just(obj_constructor.As<Function>()->StrictEquals(dom_exception_ctor));
450+
}
451+
297452
// This tells V8 how to serialize objects that it does not understand
298453
// (e.g. C++ objects) into the output buffer, in a way that our own
299454
// DeserializerDelegate understands how to unpack.
@@ -313,6 +468,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
313468
return Just(true);
314469
}
315470

471+
Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
472+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
473+
return Just(true);
474+
}
475+
316476
return Just(JSTransferable::IsJSTransferable(env_, context_, object));
317477
}
318478

@@ -328,6 +488,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
328488
return WriteHostObject(js_transferable);
329489
}
330490

491+
Maybe<bool> is_dom_exception = IsDOMException(isolate, context_, object);
492+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
493+
return WriteDOMException(context_, object);
494+
}
495+
331496
// Convert process.env to a regular object.
332497
auto env_proxy_ctor_template = env_->env_proxy_ctor_template();
333498
if (!env_proxy_ctor_template.IsEmpty() &&
@@ -424,6 +589,26 @@ class SerializerDelegate : public ValueSerializer::Delegate {
424589
ValueSerializer* serializer = nullptr;
425590

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

test/parallel/test-structuredClone-global.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,28 @@ 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+
105+
{
106+
const obj = {};
107+
obj.setPrototypeOf(DOMException.prototype);
108+
const clone = structuredClone(obj);
109+
assert.strictEqual(clone instanceof DOMException, false);
110+
}
111+
89112
const blob = new Blob();
90113
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });

0 commit comments

Comments
 (0)