Skip to content

Commit ed31a8c

Browse files
committed
src: support serialization of domexception
1 parent a2de5b9 commit ed31a8c

File tree

5 files changed

+239
-1
lines changed

5 files changed

+239
-1
lines changed

lib/internal/per_context/domexception.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const {
1313
TypeError,
1414
} = primordials;
1515

16+
/* eslint-disable no-undef */
17+
const isDomException = privateSymbols?.is_dom_exception;
18+
1619
function throwInvalidThisError(Base, type) {
1720
const err = new Base();
1821
const key = 'ERR_INVALID_THIS';
@@ -52,6 +55,10 @@ class DOMException {
5255
constructor(message = '', options = 'Error') {
5356
ErrorCaptureStackTrace(this);
5457

58+
if (isDomException) {
59+
this[isDomException] = true;
60+
}
61+
5562
if (options && typeof options === 'object') {
5663
const { name } = options;
5764
internalsMap.set(this, {

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
V(decorated_private_symbol, "node:decorated") \
2424
V(transfer_mode_private_symbol, "node:transfer_mode") \
2525
V(host_defined_option_symbol, "node:host_defined_option_symbol") \
26+
V(is_dom_exception, "node:is_dom_exception") \
2627
V(js_transferable_wrapper_private_symbol, "node:js_transferable_wrapper") \
2728
V(entry_point_module_private_symbol, "node:entry_point_module") \
2829
V(entry_point_promise_private_symbol, "node:entry_point_promise") \

src/node_messaging.cc

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ using v8::BackingStore;
1818
using v8::CompiledWasmModule;
1919
using v8::Context;
2020
using v8::EscapableHandleScope;
21+
using v8::Exception;
2122
using v8::Function;
2223
using v8::FunctionCallbackInfo;
2324
using v8::FunctionTemplate;
@@ -67,6 +68,10 @@ bool Message::IsCloseMessage() const {
6768

6869
namespace {
6970

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

92+
MaybeLocal<Object> ReadDOMException(Isolate* isolate,
93+
Local<Context> context,
94+
v8::ValueDeserializer* deserializer) {
95+
Local<Value> name, message, stack;
96+
if (!deserializer->ReadValue(context).ToLocal(&name) ||
97+
!deserializer->ReadValue(context).ToLocal(&message) ||
98+
!deserializer->ReadValue(context).ToLocal(&stack)) {
99+
return MaybeLocal<Object>();
100+
}
101+
102+
bool has_code = false;
103+
Local<Value> code;
104+
has_code = deserializer->ReadValue(context).ToLocal(&code);
105+
106+
// V8 disallows executing JS code in the deserialization process, so we
107+
// cannot create a DOMException object directly. Instead, we create a
108+
// placeholder object that will be converted to a DOMException object
109+
// later on.
110+
Local<Object> placeholder = Object::New(isolate);
111+
if (placeholder
112+
->Set(context,
113+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_name"),
114+
name)
115+
.IsNothing() ||
116+
placeholder
117+
->Set(context,
118+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_message"),
119+
message)
120+
.IsNothing() ||
121+
(has_code &&
122+
placeholder
123+
->Set(context,
124+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_code"),
125+
code)
126+
.IsNothing()) ||
127+
placeholder
128+
->Set(context,
129+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_stack"),
130+
stack)
131+
.IsNothing() ||
132+
placeholder
133+
->Set(context,
134+
FIXED_ONE_BYTE_STRING(isolate, "__domexception_placeholder"),
135+
v8::True(isolate))
136+
.IsNothing()) {
137+
return MaybeLocal<Object>();
138+
}
139+
140+
return placeholder;
141+
}
142+
87143
MaybeLocal<Object> ReadHostObject(Isolate* isolate) override {
88144
// Identifying the index in the message's BaseObject array is sufficient.
89145
uint32_t id;
90146
if (!deserializer->ReadUint32(&id))
91147
return MaybeLocal<Object>();
92-
if (id != kNormalObject) {
148+
if (id != kNormalObject && id != kDOMExceptionTag) {
93149
CHECK_LT(id, host_objects_.size());
94150
Local<Object> object = host_objects_[id]->object(isolate);
95151
if (env_->js_transferable_constructor_template()->HasInstance(object)) {
@@ -100,6 +156,9 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
100156
}
101157
EscapableHandleScope scope(isolate);
102158
Local<Context> context = isolate->GetCurrentContext();
159+
if (id == kDOMExceptionTag) {
160+
return ReadDOMException(isolate, context, deserializer);
161+
}
103162
Local<Value> object;
104163
if (!deserializer->ReadValue(context).ToLocal(&object))
105164
return MaybeLocal<Object>();
@@ -137,6 +196,72 @@ class DeserializerDelegate : public ValueDeserializer::Delegate {
137196

138197
} // anonymous namespace
139198

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

356+
Local<Object> converted_dom_exception;
357+
if (ConvertDOMExceptionData(env, return_value)
358+
.ToLocal(&converted_dom_exception)) {
359+
return handle_scope.Escape(converted_dom_exception);
360+
}
361+
231362
host_objects.clear();
232363
return handle_scope.Escape(return_value);
233364
}
@@ -297,6 +428,11 @@ void ThrowDataCloneException(Local<Context> context, Local<String> message) {
297428
isolate->ThrowException(exception);
298429
}
299430

431+
Maybe<bool> IsDOMException(Environment* env,
432+
Local<Object> obj) {
433+
return obj->HasPrivate(env->context(), env->is_dom_exception());
434+
}
435+
300436
// This tells V8 how to serialize objects that it does not understand
301437
// (e.g. C++ objects) into the output buffer, in a way that our own
302438
// DeserializerDelegate understands how to unpack.
@@ -316,6 +452,11 @@ class SerializerDelegate : public ValueSerializer::Delegate {
316452
return Just(true);
317453
}
318454

455+
Maybe<bool> is_dom_exception = IsDOMException(env_, object);
456+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
457+
return Just(true);
458+
}
459+
319460
return Just(JSTransferable::IsJSTransferable(env_, context_, object));
320461
}
321462

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

475+
Maybe<bool> is_dom_exception = IsDOMException(env_, object);
476+
if (!is_dom_exception.IsNothing() && is_dom_exception.FromJust()) {
477+
return WriteDOMException(context_, object);
478+
}
479+
334480
// Convert process.env to a regular object.
335481
auto env_proxy_ctor_template = env_->env_proxy_ctor_template();
336482
if (!env_proxy_ctor_template.IsEmpty() &&
@@ -427,6 +573,28 @@ class SerializerDelegate : public ValueSerializer::Delegate {
427573
ValueSerializer* serializer = nullptr;
428574

429575
private:
576+
Maybe<bool> WriteDOMException(Local<Context> context,
577+
Local<Object> exception) {
578+
serializer->WriteUint32(kDOMExceptionTag);
579+
580+
Local<Value> name_val, message_val, code_val, stack_val;
581+
if (!exception->Get(context, env_->name_string()).ToLocal(&name_val) ||
582+
!exception->Get(context, env_->message_string())
583+
.ToLocal(&message_val) ||
584+
!exception->Get(context, env_->stack_string()).ToLocal(&stack_val) ||
585+
!exception->Get(context, env_->code_string()).ToLocal(&code_val)) {
586+
return Nothing<bool>();
587+
}
588+
589+
if (serializer->WriteValue(context, name_val).IsNothing() ||
590+
serializer->WriteValue(context, message_val).IsNothing() ||
591+
serializer->WriteValue(context, code_val).IsNothing() ||
592+
serializer->WriteValue(context, stack_val).IsNothing()) {
593+
return Nothing<bool>();
594+
}
595+
596+
return Just(true);
597+
}
430598
Maybe<bool> WriteHostObject(BaseObjectPtr<BaseObject> host_object) {
431599
BaseObject::TransferMode mode = host_object->GetTransferMode();
432600
if (mode == TransferMode::kDisallowCloneAndTransfer) {

test/parallel/test-structuredClone-global.js

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

89+
{
90+
const [e, c] = (() => {
91+
try {
92+
structuredClone(() => {});
93+
} catch (e) {
94+
return [e, structuredClone(e)];
95+
}
96+
})();
97+
98+
assert.strictEqual(e instanceof Error, c instanceof Error);
99+
assert.strictEqual(e.name, c.name);
100+
assert.strictEqual(e.message, c.message);
101+
assert.strictEqual(e.code, c.code);
102+
}
103+
104+
{
105+
const domexception = new DOMException('test');
106+
const clone = structuredClone(domexception);
107+
const clone2 = structuredClone(clone);
108+
assert.strictEqual(clone2 instanceof DOMException, true);
109+
assert.strictEqual(clone2.message, domexception.message);
110+
assert.strictEqual(clone2.name, domexception.name);
111+
assert.strictEqual(clone2.code, domexception.code);
112+
}
113+
114+
{
115+
const obj = {};
116+
Object.setPrototypeOf(obj, DOMException.prototype);
117+
const clone = structuredClone(obj);
118+
assert.strictEqual(clone instanceof DOMException, false);
119+
}
120+
89121
const blob = new Blob();
90122
assert.throws(() => structuredClone(blob, { transfer: [blob] }), { name: 'DataCloneError' });
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const { Worker } = require('worker_threads');
7+
8+
{
9+
const domexception = new DOMException('test');
10+
const w = new Worker(`
11+
const { parentPort } = require('worker_threads');
12+
parentPort.on('message', (event) => {
13+
if (event.type === 'error') {
14+
console.log('event', event.domexception);
15+
parentPort.postMessage(event);
16+
}
17+
});
18+
`, { eval: true });
19+
20+
21+
w.on('message', common.mustCall(({ domexception: d }) => {
22+
assert.strictEqual(d.message, domexception.message);
23+
assert.strictEqual(d.name, domexception.name);
24+
assert.strictEqual(d.code, domexception.code);
25+
globalThis.gc();
26+
w.terminate();
27+
}));
28+
29+
w.postMessage({ type: 'error', domexception });
30+
}

0 commit comments

Comments
 (0)