@ 261107.5raw
current

Contributing to Undici

Guides

This is a collection of guides on how to run and update undici, and how to run different parts of the project.

Update llhttp

The HTTP parser used by undici is a WebAssembly build of llhttpgithub.

While the project itself provides a way to compile targeting WebAssembly, at the moment we embed the sources directly and compile the module in undici.

The deps/llhttp/include folder contains the C header files, while the deps/llhttp/src folder contains the C source files needed to compile the module.

The lib/llhttp folder contains the .js transpiled assets required to implement a parser.

The following are the steps required to perform an update.

Clone the llhttpgithub project

git clone git@github.com:nodejs/llhttp.git

cd llhttp

Checkout a llhttp release

git checkout <tag>

Install the llhttp dependencies

npm i

Run the wasm build script

This requires docker installed on your machine.

npm run build-wasm

Copy the sources to undici

cp build/wasm/*.js <your-path-to-undici>/lib/llhttp/

cp build/wasm/*.js.map <your-path-to-undici>/lib/llhttp/

cp build/wasm/*.d.ts <your-path-to-undici>/lib/llhttp/

cp src/native/api.c src/native/http.c build/c/llhttp.c <your-path-to-undici>/deps/llhttp/src/

cp src/native/api.h build/llhttp.h <your-path-to-undici>/deps/llhttp/include/

Build the WebAssembly module in undici

This requires docker installed on your machine.

cd <your-path-to-undici>

npm run build:llhttp

Commit the contents of lib/llhttp

Create a commit which includes all of the updated files in lib/llhttp.

Update milo

Milo is built from the upstream sources vendored in deps/milo, using its own Dockerfile rather than a prebuilt npm package. Updating it is manual, like llhttp. The standalone release CommonJS modules and their license are saved in lib/milo.

The current vendored release is 0.8.5 at commit 2ec4642184f20cd1803e30650becc54bbd575c6c.

Clone milo and checkout a release

git clone https://github.com/ShogunPanda/milo.git
cd milo
git checkout v0.8.5

Copy the sources into Undici

Replace the previous contents of deps/milo with the following files and directories from the selected release, preserving their layout:

cp Dockerfile Makefile.toml rust-toolchain.toml LICENSE.md README.md CODE_OF_CONDUCT.md <your-path-to-undici>/deps/milo/
cp -R macros scripts <your-path-to-undici>/deps/milo/
mkdir -p <your-path-to-undici>/deps/milo/parser
cp parser/*.toml parser/*.lock parser/*.md <your-path-to-undici>/deps/milo/parser/
cp -R parser/src <your-path-to-undici>/deps/milo/parser/

Keep the Cargo manifests and all three lockfiles (macros/Cargo.lock, parser/Cargo.lock and scripts/Cargo.lock), Rust binding generators and JavaScript template. Preserve Undici’s build adjustments: the Docker image digest, pinned cargo-make version, Binaryen checksums and --locked Cargo builds. Update the checksums for both Linux architectures whenever Binaryen is updated. Do not copy parser/tests, parser/examples, .git, upstream workflows, target, dist, or prebuilt npm packages. Use a clean checkout so no local build outputs are included. When selecting a newer release, update the version and full commit recorded above.

Build and copy the release artifacts

Docker is the only build tool required on the host. The upstream image installs Rust, cargo-make and Binaryen, then runs makers wasm in an internal copy of the read-only source checkout. See milo’s Docker build instructionsgithub.

From Undici, build the vendored sources:

cd <your-path-to-undici>
npm run build:milo

build/milo.js follows those instructions, mounts deps/milo read-only, and copies only these release artifacts:

release/package-cjs/src/simd/index.js    -> lib/milo/simd.js
release/package-cjs/src/no-simd/index.js -> lib/milo/no-simd.js
release/package-cjs/LICENSE.md          -> lib/milo/LICENSE.md

Each JavaScript module already embeds its WASM binary in base64. Separate .wasm files, unbundled modules, debug packages and package metadata are not copied. Temporary Docker outputs are removed when the script finishes. The script does not download or update milo’s sources; Docker still downloads the image and build dependencies as needed.

The shared-builtin CI job removes the bundled Milo modules and rebuilds them before compiling Node.js, so that it cannot silently reuse prebuilt artifacts. The npm package contains only the license and the two bundled JavaScript modules; lib/milo/binary, lib/milo/src and separate WASM files are excluded.

Submit the update

Include the updated sources in deps/milo, generated files in lib/milo and the documentation update in a manually prepared PR. This step prepares milo’s build inputs; it does not modify the runtime parser, llhttp’s build script or any workflows.

Update WPTs

undici runs a subset of the web-platform-testsgithub.

Steps:

npm run test:wpt and node test/web-platform-tests/wpt-runner.mjs setup will initialize the WPT submodule automatically when it is missing.

If you want to prepare the checkout explicitly, run:

git submodule update --init --recursive

Run the tests

Run the tests to ensure that any new failures are marked as such.

Before running the tests for the first time, you must setup the testing environment.

cd test/web-platform-tests
node wpt-runner.mjs setup

To run all tests:

npm run test:wpt

To run a subset of tests:

cd test/web-platform-tests
node wpt-runner.mjs run [filter] [filterb]

To run a single file:

cd test/web-platform-tests
node wpt-runner.mjs run /path/to/test

Debugging

Verbose logging can be enabled by setting the NODE_DEBUG flag:

npx cross-env NODE_DEBUG=UNDICI_WPT node --run test:wpt

(npx cross-env can be omitted on Linux and Mac)

Lint

npm run lint

Test

npm run test

Coverage

npm run coverage

Issuing Releases

Release is automatic on commit to main which bumps the package.json version field. Use the “Create release PR” github action to generate a release PR.

Building for externally shared node builtins

If you are packaging undici for a distro, this might help if you would like to use an unbundled version instead of bundling one in libnode.so.

To enable this, pass EXTERNAL_PATH=/path/to/global/node_modules/undici to build/llhttp.js. Pass this path with loader.js appended to --shared-builtin-undici/undici-path in Node.js’s configure.py. If building on a non-Alpine Linux distribution, you may need to also set the WASM_CC, WASM_CFLAGS, WASM_LDFLAGS and WASM_LDLIBS environment variables before running build/llhttp.js. Similarly, you can set the WASM_OPT environment variable to utilize your own wasm-opt optimizer.

Benchmarks

cd benchmarks && npm i && npm run bench

The benchmarks will be available at http://localhost:3042.

Documentation

cd docs && npm i && npm run serve

The documentation will be available at http://localhost:3000.

Reproduction

When reporting a bug, a high-quality reproduction helps maintainers diagnose and fix the issue quickly. Follow the guidelines below to create a minimal, standalone reproduction script.

What Makes a Good Reproduction

Standalone Server Pattern

Use createServer from node:http (or node:https for TLS-related bugs) to run a local server inside the same script. This avoids external service dependencies and keeps the reproduction fully self-contained.

'use strict'

const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Client } = require('undici')

// https://github.com/nodejs/undici/issues/XXXX

test('short description of the bug', { timeout: 60000 }, async (t) => {
  t.plan(1)

  // 1. Start a local HTTP server that reproduces the scenario
  const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
    // Simulate the bug-triggering response
    res.statusCode = 200
    res.setHeader('content-length', 100)
    res.end('hello'.repeat(20))
  })
  t.after(() => { server.closeAllConnections?.(); server.close() })

  server.listen(0)
  await once(server, 'listening')

  // 2. Create the undici client pointing to the local server
  const client = new Client(`http://localhost:${server.address().port}`)
  t.after(() => client.close())

  // 3. Perform the request that triggers the bug
  const { body } = await client.request({ path: '/', method: 'GET' })

  let responseBody = ''
  for await (const chunk of body) {
    responseBody += chunk
  }

  t.assert.strictEqual(responseBody, 'hello'.repeat(20))
})

Save the script as test/repro-XXXX.js and run it:

node --test test/repro-XXXX.js

Fetch-Based Reproduction

For bugs involving fetch, use the same standalone server pattern:

'use strict'

const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { fetch, Agent } = require('undici')

// https://github.com/nodejs/undici/issues/XXXX

test('short description of the fetch bug', { timeout: 60000 }, async (t) => {
  t.plan(1)

  const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
    res.statusCode = 200
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify({ ok: true }))
  })
  t.after(() => server.close())

  server.listen(0)
  await once(server, 'listening')

  const agent = new Agent({ keepAliveTimeout: 1 })
  t.after(() => agent.close())

  const response = await fetch(`http://localhost:${server.address().port}/`, {
    dispatcher: agent
  })

  await response.body.cancel()

  t.assert.strictEqual(response.status, 200)
})

Using Pool or Agent

For bugs involving connection pooling or agent behavior:

'use strict'

const { test } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Pool } = require('undici')

// https://github.com/nodejs/undici/issues/XXXX

test('Pool bug reproduction', { timeout: 60000 }, async (t) => {
  t.plan(1)

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.end('ok')
  })
  t.after(() => server.close())

  server.listen(0)
  await once(server, 'listening')

  const pool = new Pool(`http://localhost:${server.address().port}`)
  t.after(() => pool.close())

  const data = await pool.request({ path: '/', method: 'GET' })
  await data.body.dump()

  t.assert.strictEqual(data.statusCode, 200)
})

Adding the Reproduction to the Repository

If you are submitting a bug fix via a pull request, include a test file test/issue-XXXX.js that reproduces the bug and passes with the fix.

Checklist for Submitting a Bug Report

  1. Write a standalone reproduction script.
  2. Run it locally to confirm the bug is reproducible.
  3. Attach the script (or a gist link) in the bug report.
  4. Include the exact undici version and Node.js version.
  5. Describe the expected vs. actual behavior.

Developer’s Certificate of Origin 1.1

By making a contribution to this project, I certify that:

Moderation Policy

The Node.js Moderation Policygithub applies to this project.