Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
352c691
feat: expose statusText in request() ResponseData (#4784)
domenic Jan 31, 2026
5a5920d
test: reduce retry-after invalid date timing flake (#4788)
mcollina Jan 31, 2026
9948d6f
extractBody fixes (#4791)
KhafraDev Feb 1, 2026
f1d50a2
fix: MockAgent delayed response with AbortSignal (#4693) (#4772)
mcollina Feb 1, 2026
e65fa39
fix: onParserTimeout potentially accessing undefined (#4758)
vbfox Feb 1, 2026
6cc668d
Bumped v7.20.0 (#4792)
github-actions[bot] Feb 1, 2026
072c17a
build(deps): bump actions/setup-node from 6.0.0 to 6.2.0 (#4796)
dependabot[bot] Feb 1, 2026
dae4d91
test: restore global dispatcher after fetch tests (#4790)
mcollina Feb 2, 2026
ff6bc5a
Add close method to WebSocketStream interface (#4802)
piotr-cz Feb 3, 2026
fc8bb75
fix: error stream instead of canceling (#4804)
KhafraDev Feb 4, 2026
9cc025b
Fix clientTtl cleanup race (#4807)
mcollina Feb 5, 2026
283f2aa
feat(#4230): Implement pingInterval for dispatching PING frames (#4296)
metcoder95 Feb 6, 2026
92e38fc
fix: handle undefined __filename in bundled environments (#4812)
mcollina Feb 6, 2026
47f9b96
fix: set finalizer only for fetch responses (#4803)
tsctx Feb 6, 2026
393c0da
Bumped v7.21.0 (#4813)
github-actions[bot] Feb 6, 2026
b3326b5
docs: fix syntax highlighting in WebSocket.md (#4814)
styfle Feb 7, 2026
a821c56
fix: use OR operator in includesCredentials per WHATWG URL Standard (…
jackhax Feb 7, 2026
4658cdf
feat(dispatcher/env-http-proxy-agent): strip leading dot and asterisk…
SuperOleg39 Feb 7, 2026
2453caf
fix: route websocket upgrades through new handler API (#4787)
mcollina Feb 9, 2026
0ce57ba
build(deps-dev): bump esbuild from 0.25.12 to 0.27.3 (#4821)
dependabot[bot] Feb 10, 2026
09862a2
feat!: enable h2 by default
metcoder95 Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/autobahn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
args: docker restart --time 0 --signal=SIGKILL fuzzingserver

- name: Setup Node.js@${{ inputs.node-version }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ inputs.node-version }}

Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand All @@ -44,7 +44,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand All @@ -66,7 +66,7 @@ jobs:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand All @@ -87,7 +87,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand All @@ -109,7 +109,7 @@ jobs:
persist-credentials: false
ref: ${{ github.base_ref }}
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand All @@ -130,7 +130,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
- name: Install Modules for undici
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
# Using `latest` as `lts` could point to previous major version.
# Different versions of Node.js can cause different linting results
Expand Down Expand Up @@ -104,7 +104,7 @@ jobs:

# Setup node, install deps, and build undici prior to building icu-less node and testing
- name: Setup Node.js@${{ matrix.node-version }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}

Expand Down Expand Up @@ -190,7 +190,7 @@ jobs:

# Setup node, install deps, and build undici prior to building icu-less node and testing
- name: Setup Node.js@${{ matrix.node-version }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}

Expand Down Expand Up @@ -255,7 +255,7 @@ jobs:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*

Expand Down Expand Up @@ -288,7 +288,7 @@ jobs:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: lts/*

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

# Setup node, install deps, and build undici prior to building node with `--shared-builtin-undici/undici-path` and testing
- name: Setup Node.js lts/*
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
submodules: recursive

- name: Setup Node.js@${{ inputs.node-version }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ inputs.node-version }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-create-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
persist-credentials: true

- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
id: setup-node
with:
node-version: 'lts/*'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
persist-credentials: true

- name: Setup Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org'
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ Returns: `Client`
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **allowH2**: `boolean` - Default: `true`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
* **useH2c**: `boolean` - Default: `false`. Enforces h2c for non-https connections.
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.

> **Notes about HTTP/2**
> - It only works under TLS connections. h2c is not supported.
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
#### Parameter: `ResponseData`

* **statusCode** `number`
* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found").
* **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e.g. `content-type`.
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
* **trailers** `Record<string, string>` - This object starts out
Expand Down Expand Up @@ -517,7 +518,7 @@ await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)

try {
const { body, headers, statusCode, trailers } = await client.request({
const { body, headers, statusCode, statusText, trailers } = await client.request({
path: '/',
method: 'GET'
})
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/H2CClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Returns: `H2CClient`
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
- **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time.
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
- **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/api/WebSocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Arguments:

This example will not work in browsers or other platforms that don't allow passing an object.

```mjs
```js
import { WebSocket, ProxyAgent } from 'undici'

const proxyAgent = new ProxyAgent('my.proxy.server')
Expand All @@ -28,7 +28,7 @@ const ws = new WebSocket('wss://echo.websocket.events', {

If you do not need a custom Dispatcher, it's recommended to use the following pattern:

```mjs
```js
import { WebSocket } from 'undici'

const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
Expand All @@ -44,7 +44,7 @@ const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])

This example will not work in browsers or other platforms that don't allow passing an object.

```mjs
```js
import { Agent } from 'undici'

const agent = new Agent({ allowH2: true })
Expand Down
10 changes: 9 additions & 1 deletion index-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
const fetchImpl = require('./lib/web/fetch').fetch

// Capture __filename at module load time for stack trace augmentation.
// This may be undefined when bundled in environments like Node.js internals.
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined

function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
Expand All @@ -30,7 +34,11 @@ function appendFetchStackTrace (err, filename) {

module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
appendFetchStackTrace(err, __filename)
if (currentFilename) {
appendFetchStackTrace(err, currentFilename)
} else if (err && typeof err === 'object') {
Error.captureStackTrace(err, module.exports.fetch)
}
throw err
})
}
Expand Down
10 changes: 9 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ module.exports.getGlobalDispatcher = getGlobalDispatcher

const fetchImpl = require('./lib/web/fetch').fetch

// Capture __filename at module load time for stack trace augmentation.
// This may be undefined when bundled in environments like Node.js internals.
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined

function appendFetchStackTrace (err, filename) {
if (!err || typeof err !== 'object') {
return
Expand All @@ -147,7 +151,11 @@ function appendFetchStackTrace (err, filename) {

module.exports.fetch = function fetch (init, options = undefined) {
return fetchImpl(init, options).catch(err => {
appendFetchStackTrace(err, __filename)
if (currentFilename) {
appendFetchStackTrace(err, currentFilename)
} else if (err && typeof err === 'object') {
Error.captureStackTrace(err, module.exports.fetch)
}
throw err
})
}
Expand Down
1 change: 1 addition & 0 deletions lib/api/api-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class RequestHandler extends AsyncResource {
try {
this.runInAsyncScope(callback, null, null, {
statusCode,
statusText: statusMessage,
headers,
trailers: this.trailers,
opaque,
Expand Down
2 changes: 1 addition & 1 deletion lib/core/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function buildConnector ({ allowH2, useH2c, maxCachedSessions, socketPath, timeo
const options = { path: socketPath, ...opts }
const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
timeout = timeout == null ? 10e3 : timeout
allowH2 = allowH2 != null ? allowH2 : false
allowH2 = allowH2 != null ? allowH2 : true
return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
let socket
if (protocol === 'https:') {
Expand Down
1 change: 1 addition & 0 deletions lib/core/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module.exports = {
kEnableConnectProtocol: Symbol('http2session connect protocol'),
kRemoteSettings: Symbol('http2session remote settings'),
kHTTP2Stream: Symbol('http2session client stream'),
kPingInterval: Symbol('ping interval'),
kNoProxyAgent: Symbol('no proxy agent'),
kHttpProxyAgent: Symbol('http proxy agent'),
kHttpsProxyAgent: Symbol('https proxy agent')
Expand Down
4 changes: 3 additions & 1 deletion lib/dispatcher/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ class Agent extends DispatcherBase {
if (connected) result.count -= 1
if (result.count <= 0) {
this[kClients].delete(key)
result.dispatcher.close()
if (!result.dispatcher.destroyed) {
result.dispatcher.close()
}
}
this[kOrigins].delete(key)
}
Expand Down
9 changes: 7 additions & 2 deletions lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -735,8 +735,13 @@ class Parser {
}
}

function onParserTimeout (parser) {
const { socket, timeoutType, client, paused } = parser.deref()
function onParserTimeout (parserWeakRef) {
const parser = parserWeakRef.deref()
if (!parser) {
return
}

const { socket, timeoutType, client, paused } = parser

if (timeoutType === TIMEOUT_HEADERS) {
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
Expand Down
43 changes: 40 additions & 3 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
kStrictContentLength,
kOnError,
kMaxConcurrentStreams,
kPingInterval,
kHTTP2Session,
kHTTP2InitialWindowSize,
kHTTP2ConnectionWindowSize,
Expand All @@ -34,7 +35,8 @@ const {
kBodyTimeout,
kEnableConnectProtocol,
kRemoteSettings,
kHTTP2Stream
kHTTP2Stream,
kHTTP2SessionState
} = require('../core/symbols.js')
const { channels } = require('../core/diagnostics.js')

Expand Down Expand Up @@ -102,10 +104,15 @@ function connectH2 (client, socket) {
}
})

client[kSocket] = socket
session[kOpenStreams] = 0
session[kClient] = client
session[kSocket] = socket
session[kHTTP2Session] = null
session[kHTTP2SessionState] = {
ping: {
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
}
}
// We set it to true by default in a best-effort; however once connected to an H2 server
// we will check if extended CONNECT protocol is supported or not
// and set this value accordingly.
Expand Down Expand Up @@ -253,6 +260,31 @@ function onHttp2RemoteSettings (settings) {
this[kClient][kResume]()
}

function onHttp2SendPing (session) {
const state = session[kHTTP2SessionState]
if ((session.closed || session.destroyed) && state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
return
}

// If no ping sent, do nothing
session.ping(onPing.bind(session))

function onPing (err, duration) {
const client = this[kClient]
const socket = this[kClient]

if (err != null) {
const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
socket[kError] = error
client[kOnError](error)
} else {
client.emit('ping', duration)
}
}
}

function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

Expand Down Expand Up @@ -316,14 +348,19 @@ function onHttp2SessionGoAway (errorCode) {
}

function onHttp2SessionClose () {
const { [kClient]: client } = this
const { [kClient]: client, [kHTTP2SessionState]: state } = this
const { [kSocket]: socket } = client

const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))

client[kSocket] = null
client[kHTTPContext] = null

if (state.ping.interval != null) {
clearInterval(state.ping.interval)
state.ping.interval = null
}

if (client.destroyed) {
assert(client[kPending] === 0)

Expand Down
Loading
Loading