Skip to content

Commit 2fffbee

Browse files
committed
lib: decorate undici classes as platform interfaces
Node recognizes platform/host objects by counting internal slots. Undici, as a downstream module, does not have access to internal slots, and hence its instances are recognized as plain objects. This caused issues on the `structureClone` algorithm, which has few restrictions on non-platform objects. This PR tries to fix it by decorating Undici classes with the internal slots so that underlying `Serialize()` can recognize its instances as host objects. On another note, this PR consolidates the lazy loading of Undici, so that the proxied Undici classes are referential equal.
1 parent c08bb75 commit 2fffbee

File tree

4 files changed

+58
-14
lines changed

4 files changed

+58
-14
lines changed

lib/http.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const {
2626
} = primordials;
2727

2828
const { validateInteger } = require('internal/validators');
29+
const { lazyUndici } = require('internal/util');
2930
const httpAgent = require('_http_agent');
3031
const { ClientRequest } = require('_http_client');
3132
const { methods, parsers } = require('_http_common');
@@ -42,7 +43,6 @@ const {
4243
ServerResponse,
4344
} = require('_http_server');
4445
let maxHeaderSize;
45-
let undici;
4646

4747
/**
4848
* Returns a new instance of `http.Server`.
@@ -115,14 +115,6 @@ function get(url, options, cb) {
115115
return req;
116116
}
117117

118-
/**
119-
* Lazy loads WebSocket, CloseEvent and MessageEvent classes from undici
120-
* @returns {object} An object containing WebSocket, CloseEvent, and MessageEvent classes.
121-
*/
122-
function lazyUndici() {
123-
return undici ??= require('internal/deps/undici/undici');
124-
}
125-
126118
module.exports = {
127119
_connectionListener,
128120
METHODS: methods.toSorted(),

lib/internal/util.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
ObjectSetPrototypeOf,
2020
ObjectValues,
2121
Promise,
22+
Proxy,
2223
ReflectApply,
2324
ReflectConstruct,
2425
RegExpPrototypeExec,
@@ -60,6 +61,7 @@ const {
6061
privateSymbols: {
6162
arrow_message_private_symbol,
6263
decorated_private_symbol,
64+
transfer_mode_private_symbol,
6365
},
6466
sleep: _sleep,
6567
} = internalBinding('util');
@@ -614,6 +616,31 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) {
614616
});
615617
}
616618

619+
let undici;
620+
/**
621+
* Lazy load Undici module by decorating every class with serializable
622+
* private symbol so its instances can be recognized as platform objects
623+
*/
624+
function lazyUndici() {
625+
if (!undici) {
626+
undici = require('internal/deps/undici/undici');
627+
for (const mod of [
628+
'WebSocket', 'EventSource', 'FormData', 'Headers',
629+
'Request', 'Response', 'MessageEvent', 'CloseEvent']) {
630+
undici[mod] = new Proxy(undici[mod], {
631+
__proto__: null,
632+
construct(target, args, newTarget) {
633+
const obj = ReflectConstruct(target, args, newTarget);
634+
// 0 means not cloneable, nor transferable
635+
obj[transfer_mode_private_symbol] = 0;
636+
return obj;
637+
},
638+
});
639+
}
640+
}
641+
return undici;
642+
}
643+
617644
function defineLazyProperties(target, id, keys, enumerable = true) {
618645
const descriptors = { __proto__: null };
619646
let mod;
@@ -632,7 +659,15 @@ function defineLazyProperties(target, id, keys, enumerable = true) {
632659
value: `set ${key}`,
633660
});
634661
function get() {
635-
mod ??= require(id);
662+
// Undici is special as it comes from deps and we need to load it with decoration
663+
// TODO(jazelly): not hardcode this. Ideally, every deps module that is
664+
// platform specific needs to be decorated
665+
if (id === 'internal/deps/undici/undici') {
666+
mod = lazyUndici();
667+
} else {
668+
mod ??= require(id);
669+
}
670+
636671
if (lazyLoadedValue === undefined) {
637672
lazyLoadedValue = mod[key];
638673
set(lazyLoadedValue);
@@ -916,6 +951,7 @@ module.exports = {
916951
join,
917952
lazyDOMException,
918953
lazyDOMExceptionClass,
954+
lazyUndici,
919955
normalizeEncoding,
920956
once,
921957
promisify,

lib/internal/wasm_web_api.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ const {
88
ERR_WEBASSEMBLY_RESPONSE,
99
} = require('internal/errors').codes;
1010

11-
let undici;
12-
function lazyUndici() {
13-
return undici ??= require('internal/deps/undici/undici');
14-
}
11+
const { lazyUndici } = require('internal/util');
12+
1513

1614
// This is essentially an implementation of a v8::WasmStreamingCallback, except
1715
// that it is implemented in JavaScript because the fetch() implementation is

test/parallel/test-structuredClone-global.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
// Flags: --experimental-eventsource --no-experimental-websocket --experimental-websocket
4+
35
require('../common');
46
const assert = require('assert');
57

@@ -30,6 +32,22 @@ for (const StreamClass of [ReadableStream, WritableStream, TransformStream]) {
3032
assert.ok(extendedTransfer instanceof StreamClass);
3133
}
3234

35+
// Platform object that is not serializable should throw
36+
[
37+
{ platformClass: Response, brand: 'Response' },
38+
{ platformClass: Request, value: 'http://localhost', brand: 'Request' },
39+
{ platformClass: FormData, brand: 'FormData' },
40+
{ platformClass: MessageEvent, value: 'message', brand: 'MessageEvent' },
41+
{ platformClass: CloseEvent, value: 'dummy type', brand: 'CloseEvent' },
42+
{ platformClass: WebSocket, value: 'http://localhost', brand: 'WebSocket' },
43+
{ platformClass: EventSource, value: 'http://localhost', brand: 'EventSource' },
44+
].forEach((platformEntity) => {
45+
assert.throws(() => structuredClone(new platformEntity.platformClass(platformEntity.value)),
46+
new DOMException('Cannot clone object of unsupported type.', 'DataCloneError'),
47+
`Cloning ${platformEntity.brand} should throw DOMException`);
48+
49+
});
50+
3351
for (const Transferrable of [File, Blob]) {
3452
const a2 = Transferrable === File ? '' : {};
3553
const original = new Transferrable([], a2);

0 commit comments

Comments
 (0)