From b88145d7252f735098a9fc1dcc6452b376e8067f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 30 Sep 2026 16:25:48 +0200 Subject: [PATCH 1/2] chore: exclude unused milo artifacts from npm package Signed-off-by: Matteo Collina --- .npmignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.npmignore b/.npmignore index 461d334ef36..292333b5509 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,10 @@ lib/llhttp/llhttp_simd.wasm lib/llhttp/llhttp.wasm +# Milo uses bundled JS builds that embed wasm, not the standalone builds +lib/milo/binary/** +lib/milo/src/*/unbundled.js + !types/**/* !index.d.ts !docs/docs/**/* From 22743e43098e72c1f28f12ebb987dab3fb4aefdb Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 1 Oct 2026 10:13:40 +0200 Subject: [PATCH 2/2] fix: exclude experimental milo from node.js core bundle Signed-off-by: Matteo Collina --- lib/dispatcher/client-h1.js | 7 ++- test/fetch/bundle-parser.js | 56 +++++++++++++++++++ test/fixtures/core-bundle-parser.js | 84 +++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 test/fetch/bundle-parser.js create mode 100644 test/fixtures/core-bundle-parser.js diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 620ae71dd90..c37903322de 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -56,7 +56,10 @@ const { } = require('../core/symbols.js') const constants = require('../llhttp/constants.js') -const { H1Parser, getMiloEnabled } = require('./parser-h1.js') +// Node.js core bundles only llhttp; experimental Milo remains available in npm builds. +const { H1Parser, getMiloEnabled } = typeof esbuildDetection === 'undefined' + ? require('./parser-h1.js') + : {} const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const removeAllListeners = util.removeAllListeners @@ -888,7 +891,7 @@ function connectH1 (client, socket) { socket[kIdleSocketValidation] = 0 socket[kIdleSocketValidationTimeout] = null socket[kSocketUsed] = false - socket[kParser] = (client[kUseMilo] ?? getMiloEnabled()) + socket[kParser] = typeof esbuildDetection === 'undefined' && (client[kUseMilo] ?? getMiloEnabled()) ? new H1Parser(client, socket) : new Parser(client, socket, llhttpInstance) diff --git a/test/fetch/bundle-parser.js b/test/fetch/bundle-parser.js new file mode 100644 index 00000000000..aa8593859ef --- /dev/null +++ b/test/fetch/bundle-parser.js @@ -0,0 +1,56 @@ +'use strict' + +const { test } = require('node:test') +const { execFile } = require('node:child_process') +const { join } = require('node:path') +const { promisify } = require('node:util') +const { buildSync } = require('esbuild') + +const execFileAsync = promisify(execFile) +const buildOptions = { + absWorkingDir: join(__dirname, '../..'), + entryPoints: ['index-fetch.js'], + bundle: true, + platform: 'node', + outfile: 'undici-fetch.js', + keepNames: true, + metafile: true, + write: false +} + +test('Node.js core bundle excludes Milo', (t) => { + const { metafile } = buildSync({ ...buildOptions, define: { esbuildDetection: '1' } }) + const inputs = Object.keys(metafile.inputs) + + t.assert.ok(inputs.includes('lib/llhttp/llhttp-wasm.js')) + t.assert.ok(inputs.includes('lib/llhttp/llhttp_simd-wasm.js')) + t.assert.ok(!inputs.includes('lib/dispatcher/parser-h1.js')) + t.assert.ok(!inputs.some(path => path.startsWith('lib/milo/'))) +}) + +test('ordinary esbuild bundles retain experimental Milo', (t) => { + const { metafile } = buildSync(buildOptions) + const inputs = Object.keys(metafile.inputs) + + t.assert.ok(inputs.includes('lib/dispatcher/parser-h1.js')) + t.assert.ok(inputs.includes('lib/milo/src/simd/index.js')) + t.assert.ok(inputs.includes('lib/milo/src/no-simd/index.js')) +}) + +for (const simd of ['0', '1']) { + for (const milo of ['false', '1', 'true']) { + test(`Node.js core uses only llhttp with UNDICI_NO_WASM_SIMD=${simd} and UNDICI_USE_MILO=${milo}`, async (t) => { + const { stdout, stderr } = await execFileAsync(process.execPath, [join(__dirname, '../fixtures/core-bundle-parser.js')], { + env: { + ...process.env, + UNDICI_NO_WASM_SIMD: simd, + UNDICI_USE_MILO: milo + }, + timeout: 30000 + }) + + t.assert.strictEqual(stdout, '') + t.assert.strictEqual(stderr, '') + }) + } +} diff --git a/test/fixtures/core-bundle-parser.js b/test/fixtures/core-bundle-parser.js new file mode 100644 index 00000000000..1c4ba49b539 --- /dev/null +++ b/test/fixtures/core-bundle-parser.js @@ -0,0 +1,84 @@ +'use strict' + +const assert = require('node:assert/strict') +const { channel } = require('node:diagnostics_channel') +const { once } = require('node:events') +const { createServer } = require('node:http') +const { gzipSync } = require('node:zlib') + +const compiledModules = [] +const OriginalModule = WebAssembly.Module +WebAssembly.Module = new Proxy(OriginalModule, { + construct (target, args) { + const mod = Reflect.construct(target, args) + const exports = OriginalModule.exports(mod).map(entry => entry.name) + assert.ok(exports.some(name => name.startsWith('llhttp_'))) + assert.ok(!exports.some(name => name.startsWith('milo_'))) + compiledModules.push(mod) + return mod + } +}) + +const { fetch, getGlobalDispatcher, setGlobalDispatcher, EnvHttpProxyAgent } = require('../../undici-fetch') +assert.strictEqual(compiledModules.length, 0) + +let connections = 0 +channel('undici:client:connected').subscribe(({ socket }) => { + const key = Object.getOwnPropertySymbols(socket).find(symbol => symbol.description === 'parser') + assert.ok(key) + assert.strictEqual(socket[key].constructor.name, 'Parser') + connections++ +}) + +async function main () { + const server = createServer(async (req, res) => { + if (req.url === '/redirect') { + res.writeHead(302, { location: '/length' }) + res.end() + } else if (req.url === '/echo') { + const chunks = [] + for await (const chunk of req) chunks.push(chunk) + res.end(Buffer.concat(chunks)) + } else if (req.url === '/gzip') { + res.setHeader('Content-Encoding', 'gzip') + res.end(gzipSync('hello')) + } else if (req.url === '/chunked') { + res.write('hel') + res.end('lo') + } else { + res.setHeader('Content-Length', '5') + res.end('hello') + } + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const origin = `http://127.0.0.1:${server.address().port}` + const defaultDispatcher = getGlobalDispatcher() + const forcedMiloDispatcher = new EnvHttpProxyAgent({ useMilo: true, noProxy: '*' }) + + try { + for (const dispatcher of [defaultDispatcher, forcedMiloDispatcher]) { + setGlobalDispatcher(dispatcher) + const connectionsBefore = connections + for (const path of ['/length', '/chunked', '/redirect', '/gzip']) { + const response = await fetch(origin + path) + assert.strictEqual(response.status, 200) + assert.strictEqual(await response.text(), 'hello') + } + const response = await fetch(origin + '/echo', { method: 'POST', body: 'hello' }) + assert.strictEqual(response.status, 200) + assert.strictEqual(await response.text(), 'hello') + assert.ok(connections > connectionsBefore) + } + assert.ok(compiledModules.length > 0) + } finally { + setGlobalDispatcher(defaultDispatcher) + await Promise.all([defaultDispatcher.close(), forcedMiloDispatcher.close()]) + await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) + } +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +})