From 448ee28a67a699997ec574ff641cb95d8c4622e5 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Tue, 1 Oct 2024 10:44:54 +0930 Subject: [PATCH] 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. --- lib/http.js | 10 +----- lib/internal/util.js | 38 +++++++++++++++++++- lib/internal/wasm_web_api.js | 6 ++-- test/parallel/test-structuredClone-global.js | 18 ++++++++++ 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/http.js b/lib/http.js index 96ea32cf1b1034..88491bd4919ed6 100644 --- a/lib/http.js +++ b/lib/http.js @@ -26,6 +26,7 @@ const { } = primordials; const { validateInteger } = require('internal/validators'); +const { lazyUndici } = require('internal/util'); const httpAgent = require('_http_agent'); const { ClientRequest } = require('_http_client'); const { methods, parsers } = require('_http_common'); @@ -42,7 +43,6 @@ const { ServerResponse, } = require('_http_server'); let maxHeaderSize; -let undici; /** * Returns a new instance of `http.Server`. @@ -115,14 +115,6 @@ function get(url, options, cb) { return req; } -/** - * Lazy loads WebSocket, CloseEvent and MessageEvent classes from undici - * @returns {object} An object containing WebSocket, CloseEvent, and MessageEvent classes. - */ -function lazyUndici() { - return undici ??= require('internal/deps/undici/undici'); -} - module.exports = { _connectionListener, METHODS: methods.toSorted(), diff --git a/lib/internal/util.js b/lib/internal/util.js index f1f79c7ddc8c26..90381019487438 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -19,6 +19,7 @@ const { ObjectSetPrototypeOf, ObjectValues, Promise, + Proxy, ReflectApply, ReflectConstruct, RegExpPrototypeExec, @@ -60,6 +61,7 @@ const { privateSymbols: { arrow_message_private_symbol, decorated_private_symbol, + transfer_mode_private_symbol, }, sleep: _sleep, } = internalBinding('util'); @@ -614,6 +616,31 @@ function exposeGetterAndSetter(target, name, getter, setter = undefined) { }); } +/** + * Lazy load Undici module by decorating every class with serializable + * private symbol so its instances can be recognized as platform objects + */ +function lazyUndici() { + return getLazy(() => { + const undici = require('internal/deps/undici/undici'); + for (const mod of [ + 'WebSocket', 'EventSource', 'FormData', 'Headers', + 'Request', 'Response', 'MessageEvent', 'CloseEvent']) { + + undici[mod] = new Proxy(undici[mod], { + __proto__: null, + construct(target, args, newTarget) { + const obj = ReflectConstruct(target, args, newTarget); + // 0 means not cloneable, nor transferable + obj[transfer_mode_private_symbol] = 0; + return obj; + }, + }); + } + return undici; + })(); +} + function defineLazyProperties(target, id, keys, enumerable = true) { const descriptors = { __proto__: null }; let mod; @@ -632,7 +659,15 @@ function defineLazyProperties(target, id, keys, enumerable = true) { value: `set ${key}`, }); function get() { - mod ??= require(id); + // Undici is special as it comes from deps and we need to load it with decoration + // TODO(jazelly): not hardcode this. Ideally, every deps module that is + // platform specific needs to be decorated + if (id === 'internal/deps/undici/undici') { + mod = lazyUndici(); + } else { + mod ??= require(id); + } + if (lazyLoadedValue === undefined) { lazyLoadedValue = mod[key]; set(lazyLoadedValue); @@ -916,6 +951,7 @@ module.exports = { join, lazyDOMException, lazyDOMExceptionClass, + lazyUndici, normalizeEncoding, once, promisify, diff --git a/lib/internal/wasm_web_api.js b/lib/internal/wasm_web_api.js index 9c21864fa56998..3435b64e45846a 100644 --- a/lib/internal/wasm_web_api.js +++ b/lib/internal/wasm_web_api.js @@ -8,10 +8,8 @@ const { ERR_WEBASSEMBLY_RESPONSE, } = require('internal/errors').codes; -let undici; -function lazyUndici() { - return undici ??= require('internal/deps/undici/undici'); -} +const { lazyUndici } = require('internal/util'); + // This is essentially an implementation of a v8::WasmStreamingCallback, except // that it is implemented in JavaScript because the fetch() implementation is diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index 34b6abe32d3fcf..899fb7d1eab31b 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -1,5 +1,7 @@ 'use strict'; +// Flags: --experimental-eventsource --no-experimental-websocket --experimental-websocket + require('../common'); const assert = require('assert'); @@ -30,6 +32,22 @@ for (const StreamClass of [ReadableStream, WritableStream, TransformStream]) { assert.ok(extendedTransfer instanceof StreamClass); } +// Platform object that is not serializable should throw +[ + { platformClass: Response, brand: 'Response' }, + { platformClass: Request, value: 'http://localhost', brand: 'Request' }, + { platformClass: FormData, brand: 'FormData' }, + { platformClass: MessageEvent, value: 'message', brand: 'MessageEvent' }, + { platformClass: CloseEvent, value: 'dummy type', brand: 'CloseEvent' }, + { platformClass: WebSocket, value: 'http://localhost', brand: 'WebSocket' }, + { platformClass: EventSource, value: 'http://localhost', brand: 'EventSource' }, +].forEach((platformEntity) => { + assert.throws(() => structuredClone(new platformEntity.platformClass(platformEntity.value)), + new DOMException('Cannot clone object of unsupported type.', 'DataCloneError'), + `Cloning ${platformEntity.brand} should throw DOMException`); + +}); + for (const Transferrable of [File, Blob]) { const a2 = Transferrable === File ? '' : {}; const original = new Transferrable([], a2);