deps: update undici to 7.11.0

PR-URL: https://github.com/nodejs/node/pull/58859
Co-authored-by: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Node.js GitHub Bot 2025-06-29 12:42:18 -04:00 committed by GitHub
parent 4c65776a35
commit 0b88e0927f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 1419 additions and 896 deletions

View file

@ -43,6 +43,125 @@ The benchmark is a simple getting data [example](https://github.com/nodejs/undic
└────────────────────────┴─────────┴────────────────────┴────────────┴─────────────────────────┘
```
## Undici vs. Fetch
### Overview
Node.js includes a built-in `fetch()` implementation powered by undici starting from Node.js v18. However, there are important differences between using the built-in fetch and installing undici as a separate module.
### Built-in Fetch (Node.js v18+)
Node.js's built-in fetch is powered by a bundled version of undici:
```js
// Available globally in Node.js v18+
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Check the bundled undici version
console.log(process.versions.undici); // e.g., "5.28.4"
```
**Pros:**
- No additional dependencies required
- Works across different JavaScript runtimes
- Automatic compression handling (gzip, deflate, br)
- Built-in caching support (in development)
**Cons:**
- Limited to the undici version bundled with your Node.js version
- Less control over connection pooling and advanced features
- Error handling follows Web API standards (errors wrapped in `TypeError`)
- Performance overhead due to Web Streams implementation
### Undici Module
Installing undici as a separate module gives you access to the latest features and APIs:
```bash
npm install undici
```
```js
import { request, fetch, Agent, setGlobalDispatcher } from 'undici';
// Use undici.request for maximum performance
const { statusCode, headers, body } = await request('https://api.example.com/data');
const data = await body.json();
// Or use undici.fetch with custom configuration
const agent = new Agent({ keepAliveTimeout: 10000 });
setGlobalDispatcher(agent);
const response = await fetch('https://api.example.com/data');
```
**Pros:**
- Latest undici features and bug fixes
- Access to advanced APIs (`request`, `stream`, `pipeline`)
- Fine-grained control over connection pooling
- Better error handling with clearer error messages
- Superior performance, especially with `undici.request`
- HTTP/1.1 pipelining support
- Custom interceptors and middleware
- Advanced features like `ProxyAgent`, `MockAgent`
**Cons:**
- Additional dependency to manage
- Larger bundle size
### When to Use Each
#### Use Built-in Fetch When:
- You want zero dependencies
- Building isomorphic code that runs in browsers and Node.js
- Simple HTTP requests without advanced configuration
- You're okay with the undici version bundled in your Node.js version
#### Use Undici Module When:
- You need the latest undici features and performance improvements
- You require advanced connection pooling configuration
- You need APIs not available in the built-in fetch (`ProxyAgent`, `MockAgent`, etc.)
- Performance is critical (use `undici.request` for maximum speed)
- You want better error handling and debugging capabilities
- You need HTTP/1.1 pipelining or advanced interceptors
- You prefer decoupled protocol and API interfaces
### Performance Comparison
Based on benchmarks, here's the typical performance hierarchy:
1. **`undici.request()`** - Fastest, most efficient
2. **`undici.fetch()`** - Good performance, standard compliance
3. **Node.js `http`/`https`** - Baseline performance
### Migration Guide
If you're currently using built-in fetch and want to migrate to undici:
```js
// Before: Built-in fetch
const response = await fetch('https://api.example.com/data');
// After: Undici fetch (drop-in replacement)
import { fetch } from 'undici';
const response = await fetch('https://api.example.com/data');
// Or: Undici request (better performance)
import { request } from 'undici';
const { statusCode, body } = await request('https://api.example.com/data');
const data = await body.json();
```
### Version Compatibility
You can check which version of undici is bundled with your Node.js version:
```js
console.log(process.versions.undici);
```
Installing undici as a module allows you to use a newer version than what's bundled with Node.js, giving you access to the latest features and performance improvements.
## Quick Start
```js
@ -63,6 +182,44 @@ for await (const data of body) { console.log('data', data) }
console.log('trailers', trailers)
```
## Global Installation
Undici provides an `install()` function to add all WHATWG fetch classes to `globalThis`, making them available globally:
```js
import { install } from 'undici'
// Install all WHATWG fetch classes globally
install()
// Now you can use fetch classes globally without importing
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// All classes are available globally:
const headers = new Headers([['content-type', 'application/json']])
const request = new Request('https://example.com')
const formData = new FormData()
const ws = new WebSocket('wss://example.com')
const eventSource = new EventSource('https://example.com/events')
```
The `install()` function adds the following classes to `globalThis`:
- `fetch` - The fetch function
- `Headers` - HTTP headers management
- `Response` - HTTP response representation
- `Request` - HTTP request representation
- `FormData` - Form data handling
- `WebSocket` - WebSocket client
- `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events
- `EventSource` - Server-sent events client
This is useful for:
- Polyfilling environments that don't have fetch
- Ensuring consistent fetch behavior across different Node.js versions
- Making undici's implementations available globally for libraries that expect them
## Body Mixins
The `body` mixins are the most common way to format the request/response body. Mixins include:

View file

@ -13,9 +13,9 @@ The `MemoryCacheStore` stores the responses in-memory.
**Options**
- `maxSize` - The maximum total size in bytes of all stored responses. Default `Infinity`.
- `maxCount` - The maximum amount of responses to store. Default `Infinity`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
- `maxSize` - The maximum total size in bytes of all stored responses. Default `104857600` (100MB).
- `maxCount` - The maximum amount of responses to store. Default `1024`.
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `5242880` (5MB).
### Getters

View file

@ -14,14 +14,14 @@ NODE_DEBUG=undici node script.js
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: connected to nodejs.org using https:h1
UNDICI 16241: sending request to GET https://nodejs.org//
UNDICI 16241: received response to GET https://nodejs.org// - HTTP 307
UNDICI 16241: sending request to GET https://nodejs.org/
UNDICI 16241: received response to GET https://nodejs.org/ - HTTP 307
UNDICI 16241: connecting to nodejs.org using https:h1
UNDICI 16241: trailers received from GET https://nodejs.org//
UNDICI 16241: trailers received from GET https://nodejs.org/
UNDICI 16241: connected to nodejs.org using https:h1
UNDICI 16241: sending request to GET https://nodejs.org//en
UNDICI 16241: received response to GET https://nodejs.org//en - HTTP 200
UNDICI 16241: trailers received from GET https://nodejs.org//en
UNDICI 16241: sending request to GET https://nodejs.org/en
UNDICI 16241: received response to GET https://nodejs.org/en - HTTP 200
UNDICI 16241: trailers received from GET https://nodejs.org/en
```
## `fetch`
@ -36,14 +36,14 @@ NODE_DEBUG=fetch node script.js
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: connected to nodejs.org using https:h1
FETCH 16241: sending request to GET https://nodejs.org//
FETCH 16241: received response to GET https://nodejs.org// - HTTP 307
FETCH 16241: sending request to GET https://nodejs.org/
FETCH 16241: received response to GET https://nodejs.org/ - HTTP 307
FETCH 16241: connecting to nodejs.org using https:h1
FETCH 16241: trailers received from GET https://nodejs.org//
FETCH 16241: trailers received from GET https://nodejs.org/
FETCH 16241: connected to nodejs.org using https:h1
FETCH 16241: sending request to GET https://nodejs.org//en
FETCH 16241: received response to GET https://nodejs.org//en - HTTP 200
FETCH 16241: trailers received from GET https://nodejs.org//en
FETCH 16241: sending request to GET https://nodejs.org/en
FETCH 16241: received response to GET https://nodejs.org/en - HTTP 200
FETCH 16241: trailers received from GET https://nodejs.org/en
```
## `websocket`
@ -57,6 +57,6 @@ NODE_DEBUG=websocket node script.js
WEBSOCKET 18309: connecting to echo.websocket.org using https:h1
WEBSOCKET 18309: connected to echo.websocket.org using https:h1
WEBSOCKET 18309: sending request to GET https://echo.websocket.org//
WEBSOCKET 18309: sending request to GET https://echo.websocket.org/
WEBSOCKET 18309: connection opened <ip_address>
```

View file

@ -27,9 +27,22 @@ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
Note: a request is only loosely completed to a given socket.
## `undici:request:bodyChunkSent`
This message is published when a chunk of the request body is being sent.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(({ request, chunk }) => {
// request is the same object undici:request:create
})
```
## `undici:request:bodySent`
This message is published after the request body has been fully sent.
```js
import diagnosticsChannel from 'diagnostics_channel'
@ -54,6 +67,18 @@ diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, respo
})
```
## `undici:request:bodyChunkReceived`
This message is published after a chunk of the response body has been received.
```js
import diagnosticsChannel from 'diagnostics_channel'
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
// request is the same object undici:request:create
})
```
## `undici:request:trailers`
This message is published after the response body and trailers have been received, i.e. the response has been completed.

View file

@ -841,9 +841,28 @@ try {
Compose a new dispatcher from the current dispatcher and the given interceptors.
> _Notes_:
> - The order of the interceptors matters. The first interceptor will be the first to be called.
> - The order of the interceptors matters. The last interceptor will be the first to be called.
> - It is important to note that the `interceptor` function should return a function that follows the `Dispatcher.dispatch` signature.
> - Any fork of the chain of `interceptors` can lead to unexpected results.
>
> **Interceptor Stack Visualization:**
> ```
> compose([interceptor1, interceptor2, interceptor3])
>
> Request Flow:
> ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
> │ Request │───▶│interceptor3 │───▶│interceptor2 │───▶│interceptor1 │───▶│ dispatcher │
> └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ .dispatch │
> ▲ ▲ ▲ └─────────────┘
> │ │ │ ▲
> (called first) (called second) (called last) │
> │
> ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
> │ Response │◀───│interceptor3 │◀───│interceptor2 │◀───│interceptor1 │◀─────────┘
> └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
>
> The interceptors are composed in reverse order due to function composition.
> ```
Arguments:

View file

@ -0,0 +1,91 @@
# Global Installation
Undici provides an `install()` function to add all WHATWG fetch classes to `globalThis`, making them available globally without requiring imports.
## `install()`
Install all WHATWG fetch classes globally on `globalThis`.
**Example:**
```js
import { install } from 'undici'
// Install all WHATWG fetch classes globally
install()
// Now you can use fetch classes globally without importing
const response = await fetch('https://api.example.com/data')
const data = await response.json()
// All classes are available globally:
const headers = new Headers([['content-type', 'application/json']])
const request = new Request('https://example.com')
const formData = new FormData()
const ws = new WebSocket('wss://example.com')
const eventSource = new EventSource('https://example.com/events')
```
## Installed Classes
The `install()` function adds the following classes to `globalThis`:
| Class | Description |
|-------|-------------|
| `fetch` | The fetch function for making HTTP requests |
| `Headers` | HTTP headers management |
| `Response` | HTTP response representation |
| `Request` | HTTP request representation |
| `FormData` | Form data handling |
| `WebSocket` | WebSocket client |
| `CloseEvent` | WebSocket close event |
| `ErrorEvent` | WebSocket error event |
| `MessageEvent` | WebSocket message event |
| `EventSource` | Server-sent events client |
## Use Cases
Global installation is useful for:
- **Polyfilling environments** that don't have native fetch support
- **Ensuring consistent behavior** across different Node.js versions
- **Library compatibility** when third-party libraries expect global fetch
- **Migration scenarios** where you want to replace built-in implementations
- **Testing environments** where you need predictable fetch behavior
## Example: Polyfilling an Environment
```js
import { install } from 'undici'
// Check if fetch is available and install if needed
if (typeof globalThis.fetch === 'undefined') {
install()
console.log('Undici fetch installed globally')
}
// Now fetch is guaranteed to be available
const response = await fetch('https://api.example.com')
```
## Example: Testing Environment
```js
import { install } from 'undici'
// In test setup, ensure consistent fetch behavior
install()
// Now all tests use undici's implementations
test('fetch API test', async () => {
const response = await fetch('https://example.com')
expect(response).toBeInstanceOf(Response)
})
```
## Notes
- The `install()` function overwrites any existing global implementations
- Classes installed are undici's implementations, not Node.js built-ins
- This provides access to undici's latest features and performance improvements
- The global installation persists for the lifetime of the process

View file

@ -38,6 +38,10 @@ const mockClient = mockAgent.get('http://localhost:3000')
Implements: [`MockPool.intercept(options)`](/docs/docs/api/MockPool.md#mockpoolinterceptoptions)
### `MockClient.cleanMocks()`
Implements: [`MockPool.cleanMocks()`](/docs/docs/api/MockPool.md#mockpoolcleanmocks)
### `MockClient.close()`
Implements: [`MockPool.close()`](/docs/docs/api/MockPool.md#mockpoolclose)

View file

@ -546,3 +546,9 @@ for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```
### `MockPool.cleanMocks()`
This method cleans up all the prepared mocks.
Returns: `void`

View file

@ -17,6 +17,8 @@ Returns: `ProxyAgent`
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
> It ommits `AgentOptions#connect`.
> **Note:** When `AgentOptions#connections` is set, and different from `0`, the non-standard [`proxy-connection` header](https://udger.com/resources/http-request-headers-detail?header=Proxy-Connection) will be set to `keep-alive` in the request.
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org).
For detailed information on the parsing process and potential validation errors, please refer to the ["Writing" section](https://url.spec.whatwg.org/#writing) of the WHATWG URL Specification.

View file

@ -16,6 +16,7 @@ Returns: `ProxyAgent`
### Parameter: `RetryHandlerOptions`
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler. Default: `true`
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
@ -39,7 +40,11 @@ import { Agent, RetryAgent } from 'undici'
const agent = new RetryAgent(new Agent())
const res = await agent.request('http://example.com')
const res = await agent.request({
method: 'GET',
origin: 'http://example.com',
path: '/',
})
console.log(res.statusCode)
console.log(await res.body.text())
```

View file

@ -19,6 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dis
#### `RetryOptions`
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler.
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)

View file

@ -181,3 +181,18 @@ module.exports.mockErrors = mockErrors
const { EventSource } = require('./lib/web/eventsource/eventsource')
module.exports.EventSource = EventSource
function install () {
globalThis.fetch = module.exports.fetch
globalThis.Headers = module.exports.Headers
globalThis.Response = module.exports.Response
globalThis.Request = module.exports.Request
globalThis.FormData = module.exports.FormData
globalThis.WebSocket = module.exports.WebSocket
globalThis.CloseEvent = module.exports.CloseEvent
globalThis.ErrorEvent = module.exports.ErrorEvent
globalThis.MessageEvent = module.exports.MessageEvent
globalThis.EventSource = module.exports.EventSource
}
module.exports.install = install

View file

@ -117,7 +117,7 @@ class StreamHandler extends AsyncResource {
const { callback, res, opaque, trailers, abort } = this
this.res = null
if (err || !res.readable) {
if (err || !res?.readable) {
util.destroy(res, err)
}

View file

@ -16,9 +16,9 @@ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
* @extends {EventEmitter}
*/
class MemoryCacheStore extends EventEmitter {
#maxCount = Infinity
#maxSize = Infinity
#maxEntrySize = Infinity
#maxCount = 1024
#maxSize = 104857600 // 100MB
#maxEntrySize = 5242880 // 5MB
#size = 0
#count = 0

View file

@ -1,6 +1,6 @@
'use strict'
const { Writable } = require('stream')
const { Writable } = require('node:stream')
const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
let DatabaseSync

View file

@ -12,64 +12,34 @@ let tls // include tls conditionally since it is not always available
// resolve the same servername multiple times even when
// re-use is enabled.
let SessionCache
// FIXME: remove workaround when the Node bug is fixed
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
this._sessionRegistry = new global.FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return
}
const ref = this._sessionCache.get(key)
if (ref !== undefined && ref.deref() === undefined) {
this._sessionCache.delete(key)
}
})
}
get (sessionKey) {
const ref = this._sessionCache.get(sessionKey)
return ref ? ref.deref() : null
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
const SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
this._sessionRegistry = new FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return
}
this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
const ref = this._sessionCache.get(key)
if (ref !== undefined && ref.deref() === undefined) {
this._sessionCache.delete(key)
}
})
}
} else {
SessionCache = class SimpleSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
this._sessionCache = new Map()
get (sessionKey) {
const ref = this._sessionCache.get(sessionKey)
return ref ? ref.deref() : null
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}
get (sessionKey) {
return this._sessionCache.get(sessionKey)
}
set (sessionKey, session) {
if (this._maxCachedSessions === 0) {
return
}
if (this._sessionCache.size >= this._maxCachedSessions) {
// remove the oldest session
const { value: oldestKey } = this._sessionCache.keys().next()
this._sessionCache.delete(oldestKey)
}
this._sessionCache.set(sessionKey, session)
}
this._sessionCache.set(sessionKey, new WeakRef(session))
this._sessionRegistry.register(session, sessionKey)
}
}

View file

@ -16,6 +16,8 @@ const channels = {
// Request
create: diagnosticsChannel.channel('undici:request:create'),
bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'),
bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'),
headers: diagnosticsChannel.channel('undici:request:headers'),
trailers: diagnosticsChannel.channel('undici:request:trailers'),
error: diagnosticsChannel.channel('undici:request:error'),
@ -85,7 +87,7 @@ function trackClientEvents (debugLog = undiciDebugLog) {
const {
request: { method, path, origin }
} = evt
debugLog('sending request to %s %s/%s', method, origin, path)
debugLog('sending request to %s %s%s', method, origin, path)
})
}
@ -105,7 +107,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
response: { statusCode }
} = evt
debugLog(
'received response to %s %s/%s - HTTP %d',
'received response to %s %s%s - HTTP %d',
method,
origin,
path,
@ -118,7 +120,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
const {
request: { method, path, origin }
} = evt
debugLog('trailers received from %s %s/%s', method, origin, path)
debugLog('trailers received from %s %s%s', method, origin, path)
})
diagnosticsChannel.subscribe('undici:request:error',
@ -128,7 +130,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
error
} = evt
debugLog(
'request to %s %s/%s errored - %s',
'request to %s %s%s errored - %s',
method,
origin,
path,

View file

@ -194,6 +194,9 @@ class Request {
}
onBodySent (chunk) {
if (channels.bodyChunkSent.hasSubscribers) {
channels.bodyChunkSent.publish({ request: this, chunk })
}
if (this[kHandler].onBodySent) {
try {
return this[kHandler].onBodySent(chunk)
@ -252,6 +255,9 @@ class Request {
assert(!this.aborted)
assert(!this.completed)
if (channels.bodyChunkReceived.hasSubscribers) {
channels.bodyChunkReceived.publish({ request: this, chunk })
}
try {
return this[kHandler].onData(chunk)
} catch (err) {

View file

@ -6,7 +6,6 @@ const { IncomingMessage } = require('node:http')
const stream = require('node:stream')
const net = require('node:net')
const { Blob } = require('node:buffer')
const nodeUtil = require('node:util')
const { stringify } = require('node:querystring')
const { EventEmitter: EE } = require('node:events')
const timers = require('../util/timers')
@ -660,48 +659,6 @@ function addAbortListener (signal, listener) {
return () => signal.removeListener('abort', listener)
}
/**
* @function
* @param {string} value
* @returns {string}
*/
const toUSVString = (() => {
if (typeof String.prototype.toWellFormed === 'function') {
/**
* @param {string} value
* @returns {string}
*/
return (value) => `${value}`.toWellFormed()
} else {
/**
* @param {string} value
* @returns {string}
*/
return nodeUtil.toUSVString
}
})()
/**
* @param {*} value
* @returns {boolean}
*/
// TODO: move this to webidl
const isUSVString = (() => {
if (typeof String.prototype.isWellFormed === 'function') {
/**
* @param {*} value
* @returns {boolean}
*/
return (value) => `${value}`.isWellFormed()
} else {
/**
* @param {*} value
* @returns {boolean}
*/
return (value) => toUSVString(value) === `${value}`
}
})()
/**
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
* @param {number} c
@ -943,8 +900,6 @@ Object.setPrototypeOf(normalizedMethodRecords, null)
module.exports = {
kEnumerableProperty,
isDisturbed,
toUSVString,
isUSVString,
isBlobLike,
parseOrigin,
parseURL,

View file

@ -249,7 +249,7 @@ class Parser {
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
this.timeout.unref()
this.timeout?.unref()
}
}

View file

@ -144,7 +144,8 @@ class ProxyAgent extends DispatcherBase {
signal: opts.signal,
headers: {
...this[kProxyHeaders],
host: opts.host
host: opts.host,
...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
},
servername: this[kProxyTls]?.servername || proxyHostname
})

View file

@ -29,13 +29,16 @@ class RetryHandler {
methods,
errorCodes,
retryAfter,
statusCodes
statusCodes,
throwOnError
} = retryOptions ?? {}
this.error = null
this.dispatch = dispatch
this.handler = WrapHandler.wrap(handler)
this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
this.retryOpts = {
throwOnError: throwOnError ?? true,
retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
retryAfter: retryAfter ?? true,
maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
@ -68,6 +71,50 @@ class RetryHandler {
this.etag = null
}
onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
if (this.retryOpts.throwOnError) {
// Preserve old behavior for status codes that are not eligible for retry
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
} else {
this.error = err
}
return
}
if (isDisturbed(this.opts.body)) {
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
return
}
function shouldRetry (passedErr) {
if (passedErr) {
this.headersSent = true
this.headersSent = true
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
controller.resume()
return
}
this.error = err
controller.resume()
}
controller.pause()
this.retryOpts.retry(
err,
{
state: { counter: this.retryCount },
opts: { retryOptions: this.retryOpts, ...this.opts }
},
shouldRetry.bind(this)
)
}
onRequestStart (controller, context) {
if (!this.headersSent) {
this.handler.onRequestStart?.(controller, context)
@ -137,26 +184,19 @@ class RetryHandler {
}
onResponseStart (controller, statusCode, headers, statusMessage) {
this.error = null
this.retryCount += 1
if (statusCode >= 300) {
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
this.headersSent = true
this.handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
return
} else {
throw new RequestRetryError('Request failed', statusCode, {
headers,
data: {
count: this.retryCount
}
})
}
const err = new RequestRetryError('Request failed', statusCode, {
headers,
data: {
count: this.retryCount
}
})
this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
return
}
// Checkpoint for resume from where we left it
@ -175,6 +215,7 @@ class RetryHandler {
const contentRange = parseRangeHeader(headers['content-range'])
// If no content range
if (!contentRange) {
// We always throw here as we want to indicate that we entred unexpected path
throw new RequestRetryError('Content-Range mismatch', statusCode, {
headers,
data: { count: this.retryCount }
@ -183,6 +224,7 @@ class RetryHandler {
// Let's start with a weak etag check
if (this.etag != null && this.etag !== headers.etag) {
// We always throw here as we want to indicate that we entred unexpected path
throw new RequestRetryError('ETag mismatch', statusCode, {
headers,
data: { count: this.retryCount }
@ -266,14 +308,52 @@ class RetryHandler {
}
onResponseData (controller, chunk) {
if (this.error) {
return
}
this.start += chunk.length
this.handler.onResponseData?.(controller, chunk)
}
onResponseEnd (controller, trailers) {
this.retryCount = 0
return this.handler.onResponseEnd?.(controller, trailers)
if (this.error && this.retryOpts.throwOnError) {
throw this.error
}
if (!this.error) {
this.retryCount = 0
return this.handler.onResponseEnd?.(controller, trailers)
}
this.retry(controller)
}
retry (controller) {
if (this.start !== 0) {
const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
// Weak etag check - weak etags will make comparison algorithms never match
if (this.etag != null) {
headers['if-match'] = this.etag
}
this.opts = {
...this.opts,
headers: {
...this.opts.headers,
...headers
}
}
}
try {
this.retryCountCheckpoint = this.retryCount
this.dispatch(this.opts, this)
} catch (err) {
this.handler.onResponseError?.(controller, err)
}
}
onResponseError (controller, err) {
@ -282,6 +362,15 @@ class RetryHandler {
return
}
function shouldRetry (returnedErr) {
if (!returnedErr) {
this.retry(controller)
return
}
this.handler?.onResponseError?.(controller, returnedErr)
}
// We reconcile in case of a mix between network errors
// and server error response
if (this.retryCount - this.retryCountCheckpoint > 0) {
@ -299,43 +388,8 @@ class RetryHandler {
state: { counter: this.retryCount },
opts: { retryOptions: this.retryOpts, ...this.opts }
},
onRetry.bind(this)
shouldRetry.bind(this)
)
/**
* @this {RetryHandler}
* @param {Error} [err]
* @returns
*/
function onRetry (err) {
if (err != null || controller?.aborted || isDisturbed(this.opts.body)) {
return this.handler.onResponseError?.(controller, err)
}
if (this.start !== 0) {
const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
// Weak etag check - weak etags will make comparison algorithms never match
if (this.etag != null) {
headers['if-match'] = this.etag
}
this.opts = {
...this.opts,
headers: {
...this.opts.headers,
...headers
}
}
}
try {
this.retryCountCheckpoint = this.retryCount
this.dispatch(this.opts, this)
} catch (err) {
this.handler.onResponseError?.(controller, err)
}
}
}
}

View file

@ -1,5 +1,5 @@
> undici@7.10.0 build:wasm
> undici@7.11.0 build:wasm
> node build/wasm.js --docker
> docker run --rm --platform=linux/x86_64 --user 1001:118 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js

View file

@ -54,6 +54,10 @@ class MockClient extends Client {
)
}
cleanMocks () {
this[kDispatches] = []
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0

View file

@ -54,6 +54,10 @@ class MockPool extends Pool {
)
}
cleanMocks () {
this[kDispatches] = []
}
async [kClose] () {
await promisify(this[kOriginalClose])()
this[kConnected] = 0

View file

@ -4,6 +4,8 @@ const {
safeHTTPMethods
} = require('../core/util')
const { serializePathWithQuery } = require('../core/util')
/**
* @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts
*/
@ -12,10 +14,18 @@ function makeCacheKey (opts) {
throw new Error('opts.origin is undefined')
}
let fullPath
try {
fullPath = serializePathWithQuery(opts.path || '/', opts.query)
} catch (error) {
// If fails (path already has query params), use as-is
fullPath = opts.path || '/'
}
return {
origin: opts.origin.toString(),
method: opts.method,
path: opts.path,
path: fullPath,
headers: opts.headers
}
}

View file

@ -188,19 +188,21 @@ function onTick () {
}
function refreshTimeout () {
// If the fastNowTimeout is already set, refresh it.
if (fastNowTimeout) {
// If the fastNowTimeout is already set and the Timer has the refresh()-
// method available, call it to refresh the timer.
// Some timer objects returned by setTimeout may not have a .refresh()
// method (e.g. mocked timers in tests).
if (fastNowTimeout?.refresh) {
fastNowTimeout.refresh()
// fastNowTimeout is not instantiated yet, create a new Timer.
// fastNowTimeout is not instantiated yet or refresh is not availabe,
// create a new Timer.
} else {
clearTimeout(fastNowTimeout)
fastNowTimeout = setTimeout(onTick, TICK_MS)
// If the Timer has an unref method, call it to allow the process to exit if
// there are no other active handles.
if (fastNowTimeout.unref) {
fastNowTimeout.unref()
}
// If the Timer has an unref method, call it to allow the process to exit,
// if there are no other active handles. When using fake timers or mocked
// environments (like Jest), .unref() may not be defined,
fastNowTimeout?.unref()
}
}

View file

@ -3,7 +3,7 @@
const { kConstruct } = require('../../core/symbols')
const { urlEquals, getFieldValues } = require('./util')
const { kEnumerableProperty, isDisturbed } = require('../../core/util')
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { cloneResponse, fromInnerResponse, getResponseState } = require('../fetch/response')
const { Request, fromInnerRequest, getRequestState } = require('../fetch/request')
const { fetching } = require('../fetch/index')

View file

@ -1,7 +1,7 @@
'use strict'
const { Cache } = require('./cache')
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')

View file

@ -2,7 +2,7 @@
const { parseSetCookie } = require('./parse')
const { stringify } = require('./util')
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { Headers } = require('../fetch/headers')
const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean))

View file

@ -3,7 +3,7 @@
const { pipeline } = require('node:stream')
const { fetching } = require('../fetch')
const { makeRequest } = require('../fetch/request')
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { EventSourceStream } = require('./eventsource-stream')
const { parseMIMEType } = require('../fetch/data-url')
const { createFastMessageEvent } = require('../websocket/events')
@ -231,12 +231,9 @@ class EventSource extends EventTarget {
// 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
const processEventSourceEndOfBody = (response) => {
if (isNetworkError(response)) {
this.dispatchEvent(new Event('error'))
this.close()
if (!isNetworkError(response)) {
return this.#reconnect()
}
this.#reconnect()
}
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...

View file

@ -26,7 +26,7 @@ function isASCIINumber (value) {
// https://github.com/nodejs/undici/issues/2664
function delay (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms).unref()
setTimeout(resolve, ms)
})
}

View file

@ -10,7 +10,7 @@ const {
utf8DecodeBytes
} = require('./util')
const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const { Blob } = require('node:buffer')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
@ -29,7 +29,7 @@ try {
const textEncoder = new TextEncoder()
function noop () {}
const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
const hasFinalizationRegistry = globalThis.FinalizationRegistry
let streamRegistry
if (hasFinalizationRegistry) {

View file

@ -1,46 +1,5 @@
'use strict'
const { kConnected, kSize } = require('../../core/symbols')
class CompatWeakRef {
constructor (value) {
this.value = value
}
deref () {
return this.value[kConnected] === 0 && this.value[kSize] === 0
? undefined
: this.value
}
}
class CompatFinalizer {
constructor (finalizer) {
this.finalizer = finalizer
}
register (dispatcher, key) {
if (dispatcher.on) {
dispatcher.on('disconnect', () => {
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
this.finalizer(key)
}
})
}
}
unregister (key) {}
}
module.exports = function () {
// FIXME: remove workaround when the Node bug is backported to v18
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
if (process.env.NODE_V8_COVERAGE && process.version.startsWith('v18')) {
process._rawDebug('Using compatibility WeakRef and FinalizationRegistry')
return {
WeakRef: CompatWeakRef,
FinalizationRegistry: CompatFinalizer
}
}
return { WeakRef, FinalizationRegistry }
}

View file

@ -1,10 +1,10 @@
'use strict'
const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
const { bufferToLowerCasedHeaderName } = require('../../core/util')
const { utf8DecodeBytes } = require('./util')
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
const { makeEntry } = require('./formdata')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { File: NodeFile } = require('node:buffer')
@ -200,8 +200,8 @@ function multipartFormDataParser (input, mimeType) {
}
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
assert(isUSVString(name))
assert((typeof value === 'string' && isUSVString(value)) || webidl.is.File(value))
assert(webidl.is.USVString(name))
assert((typeof value === 'string' && webidl.is.USVString(value)) || webidl.is.File(value))
// 5.13. Create an entry with name and value, and append it to entry list.
entryList.push(makeEntry(name, value, filename))

View file

@ -2,7 +2,7 @@
const { iteratorMixin } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const { File: NativeFile } = require('node:buffer')
const nodeUtil = require('node:util')

View file

@ -9,7 +9,7 @@ const {
isValidHeaderName,
isValidHeaderValue
} = require('./util')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const util = require('node:util')

View file

@ -61,7 +61,7 @@ const { Readable, pipeline, finished, isErrored, isReadable } = require('node:st
const { addAbortListener, bufferToLowerCasedHeaderName } = require('../../core/util')
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url')
const { getGlobalDispatcher } = require('../../global')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const { STATUS_CODES } = require('node:http')
const GET_OR_HEAD = ['GET', 'HEAD']
@ -2155,6 +2155,12 @@ async function httpNetworkFetch (
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}))
} else if (coding === 'zstd' && typeof zlib.createZstdDecompress === 'function') {
// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
decoders.push(zlib.createZstdDecompress({
flush: zlib.constants.ZSTD_e_continue,
finishFlush: zlib.constants.ZSTD_e_end
}))
} else {
decoders.length = 0
break

View file

@ -23,7 +23,7 @@ const {
requestDuplex
} = require('./constants')
const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const { URLSerializer } = require('./data-url')
const { kConstruct } = require('../../core/symbols')
const assert = require('node:assert')

View file

@ -18,7 +18,7 @@ const {
redirectStatusSet,
nullBodyStatus
} = require('./constants')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
const { URLSerializer } = require('./data-url')
const { kConstruct } = require('../../core/symbols')
const assert = require('node:assert')

View file

@ -9,7 +9,7 @@ const { performance } = require('node:perf_hooks')
const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
const assert = require('node:assert')
const { isUint8Array } = require('node:util/types')
const { webidl } = require('./webidl')
const { webidl } = require('../webidl')
let supportedHashes = []
@ -1262,7 +1262,7 @@ async function readAllBytes (reader, successSteps, failureSteps) {
// 1. If chunk is not a Uint8Array object, call failureSteps
// with a TypeError and abort these steps.
if (!isUint8Array(chunk)) {
failureSteps(TypeError('Received non-Uint8Array chunk'))
failureSteps(new TypeError('Received non-Uint8Array chunk'))
return
}

View file

@ -2,7 +2,6 @@
const { types, inspect } = require('node:util')
const { markAsUncloneable } = require('node:worker_threads')
const { toUSVString } = require('../../core/util')
const UNDEFINED = 1
const BOOLEAN = 2
@ -23,22 +22,48 @@ const webidl = {
is: {}
}
/**
* @description Instantiate an error.
*
* @param {Object} opts
* @param {string} opts.header
* @param {string} opts.message
* @returns {TypeError}
*/
webidl.errors.exception = function (message) {
return new TypeError(`${message.header}: ${message.message}`)
}
webidl.errors.conversionFailed = function (context) {
const plural = context.types.length === 1 ? '' : ' one of'
/**
* @description Instantiate an error when conversion from one type to another has failed.
*
* @param {Object} opts
* @param {string} opts.prefix
* @param {string} opts.argument
* @param {string[]} opts.types
* @returns {TypeError}
*/
webidl.errors.conversionFailed = function (opts) {
const plural = opts.types.length === 1 ? '' : ' one of'
const message =
`${context.argument} could not be converted to` +
`${plural}: ${context.types.join(', ')}.`
`${opts.argument} could not be converted to` +
`${plural}: ${opts.types.join(', ')}.`
return webidl.errors.exception({
header: context.prefix,
header: opts.prefix,
message
})
}
/**
* @description Instantiate an error when an invalid argument is provided
*
* @param {Object} context
* @param {string} context.prefix
* @param {string} context.value
* @param {string} context.type
* @returns {TypeError}
*/
webidl.errors.invalidArgument = function (context) {
return webidl.errors.exception({
header: context.prefix,
@ -278,6 +303,8 @@ webidl.util.Stringify = function (V) {
return inspect(V)
case STRING:
return `"${V}"`
case BIGINT:
return `${V}n`
default:
return `${V}`
}
@ -468,6 +495,17 @@ webidl.nullableConverter = function (converter) {
}
}
/**
* @param {*} value
* @returns {boolean}
*/
webidl.is.USVString = function (value) {
return (
typeof value === 'string' &&
value.isWellFormed()
)
}
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
@ -529,13 +567,23 @@ webidl.converters.ByteString = function (V, prefix, argument) {
return x
}
// https://webidl.spec.whatwg.org/#es-USVString
// TODO: rewrite this so we can control the errors thrown
webidl.converters.USVString = toUSVString
/**
* @param {unknown} value
* @returns {string}
* @see https://webidl.spec.whatwg.org/#es-USVString
*/
webidl.converters.USVString = function (value) {
// TODO: rewrite this so we can control the errors thrown
if (typeof value === 'string') {
return value.toWellFormed()
}
return `${value}`.toWellFormed()
}
// https://webidl.spec.whatwg.org/#es-boolean
webidl.converters.boolean = function (V) {
// 1. Let x be the result of computing ToBoolean(V).
// https://262.ecma-international.org/10.0/index.html#table-10
const x = Boolean(V)
// 2. Return the IDL boolean value that is the one that represents

View file

@ -105,7 +105,7 @@ function establishWebSocketConnection (url, protocols, client, handler, options)
// 1. If response is a network error or its status is not 101,
// fail the WebSocket connection.
if (response.type === 'error' || response.status !== 101) {
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.')
failWebsocketConnection(handler, 1002, 'Received network error or non-101 status code.', response.error)
return
}
@ -298,9 +298,10 @@ function closeWebSocketConnection (object, code, reason, validate = false) {
* @param {import('./websocket').Handler} handler
* @param {number} code
* @param {string|undefined} reason
* @param {unknown} cause
* @returns {void}
*/
function failWebsocketConnection (handler, code, reason) {
function failWebsocketConnection (handler, code, reason, cause) {
// If _The WebSocket Connection is Established_ prior to the point where
// the endpoint is required to _Fail the WebSocket Connection_, the
// endpoint SHOULD send a Close frame with an appropriate status code
@ -315,7 +316,7 @@ function failWebsocketConnection (handler, code, reason) {
handler.socket.destroy()
}
handler.onFail(code, reason)
handler.onFail(code, reason, cause)
}
module.exports = {

View file

@ -1,6 +1,6 @@
'use strict'
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { kEnumerableProperty } = require('../../core/util')
const { kConstruct } = require('../../core/symbols')

View file

@ -134,5 +134,6 @@ class WebsocketFrameSend {
}
module.exports = {
WebsocketFrameSend
WebsocketFrameSend,
generateMask // for benchmark
}

View file

@ -1,6 +1,6 @@
'use strict'
const { webidl } = require('../../fetch/webidl')
const { webidl } = require('../../webidl')
const { validateCloseCodeAndReason } = require('../util')
const { kConstruct } = require('../../../core/symbols')
const { kEnumerableProperty } = require('../../../core/util')

View file

@ -2,7 +2,7 @@
const { createDeferredPromise, environmentSettingsObject } = require('../../fetch/util')
const { states, opcodes, sentCloseFrameState } = require('../constants')
const { webidl } = require('../../fetch/webidl')
const { webidl } = require('../../webidl')
const { getURLRecord, isValidSubprotocol, isEstablished, utf8Decode } = require('../util')
const { establishWebSocketConnection, failWebsocketConnection, closeWebSocketConnection } = require('../connection')
const { types } = require('node:util')

View file

@ -1,6 +1,6 @@
'use strict'
const { webidl } = require('../fetch/webidl')
const { webidl } = require('../webidl')
const { URLSerializer } = require('../fetch/data-url')
const { environmentSettingsObject } = require('../fetch/util')
const { staticPropertyDescriptors, states, sentCloseFrameState, sendHints, opcodes } = require('./constants')
@ -60,7 +60,7 @@ class WebSocket extends EventTarget {
/** @type {Handler} */
#handler = {
onConnectionEstablished: (response, extensions) => this.#onConnectionEstablished(response, extensions),
onFail: (code, reason) => this.#onFail(code, reason),
onFail: (code, reason, cause) => this.#onFail(code, reason, cause),
onMessage: (opcode, data) => this.#onMessage(opcode, data),
onParserError: (err) => failWebsocketConnection(this.#handler, null, err.message),
onParserDrain: () => this.#onParserDrain(),
@ -462,11 +462,11 @@ class WebSocket extends EventTarget {
fireEvent('open', this)
}
#onFail (code, reason) {
#onFail (code, reason, cause) {
if (reason) {
// TODO: process.nextTick
fireEvent('error', this, (type, init) => new ErrorEvent(type, init), {
error: new Error(reason),
error: new Error(reason, cause ? { cause } : undefined),
message: reason
})
}

1088
deps/undici/src/package-lock.json generated vendored

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "undici",
"version": "7.10.0",
"version": "7.11.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
@ -112,20 +112,20 @@
"@sinonjs/fake-timers": "^12.0.0",
"@types/node": "^18.19.50",
"abort-controller": "^3.0.0",
"borp": "^0.19.0",
"borp": "^0.20.0",
"c8": "^10.0.0",
"cross-env": "^7.0.3",
"dns-packet": "^5.4.0",
"esbuild": "^0.25.2",
"eslint": "^9.9.0",
"fast-check": "^3.17.1",
"fast-check": "^4.1.1",
"https-pem": "^3.0.0",
"husky": "^9.0.7",
"jest": "^29.0.2",
"neostandard": "^0.12.0",
"node-forge": "^1.3.1",
"proxy": "^2.1.1",
"tsd": "^0.31.2",
"tsd": "^0.32.0",
"typescript": "^5.6.2",
"ws": "^8.11.0"
},

View file

@ -31,6 +31,15 @@ declare namespace DiagnosticsChannel {
export interface RequestBodySentMessage {
request: Request;
}
export interface RequestBodyChunkSentMessage {
request: Request;
chunk: Uint8Array | string;
}
export interface RequestBodyChunkReceivedMessage {
request: Request;
chunk: Buffer;
}
export interface RequestHeadersMessage {
request: Request;
response: Response;

View file

@ -97,7 +97,8 @@ declare class Dispatcher extends EventEmitter {
declare namespace Dispatcher {
export interface ComposedDispatcher extends Dispatcher {}
export type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']
export type Dispatch = Dispatcher['dispatch']
export type DispatcherComposeInterceptor = (dispatch: Dispatch) => Dispatch
export interface DispatchOptions {
origin?: string | URL;
path: string;
@ -276,6 +277,6 @@ declare namespace Dispatcher {
}
export interface DispatchInterceptor {
(dispatch: Dispatcher['dispatch']): Dispatcher['dispatch']
(dispatch: Dispatch): Dispatch
}
}

View file

@ -1,4 +1,5 @@
import Agent from './agent'
import ProxyAgent from './proxy-agent'
import Dispatcher from './dispatcher'
export default EnvHttpProxyAgent
@ -10,7 +11,7 @@ declare class EnvHttpProxyAgent extends Dispatcher {
}
declare namespace EnvHttpProxyAgent {
export interface Options extends Agent.Options {
export interface Options extends Omit<ProxyAgent.Options, 'uri'> {
/** Overrides the value of the HTTP_PROXY environment variable */
httpProxy?: string;
/** Overrides the value of the HTTPS_PROXY environment variable */

View file

@ -18,9 +18,9 @@ interface EventSource extends EventTarget {
readonly CLOSED: 2
readonly CONNECTING: 0
readonly OPEN: 1
onerror: (this: EventSource, ev: ErrorEvent) => any
onmessage: (this: EventSource, ev: MessageEvent) => any
onopen: (this: EventSource, ev: Event) => any
onerror: ((this: EventSource, ev: ErrorEvent) => any) | null
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null
onopen: ((this: EventSource, ev: Event) => any) | null
readonly readyState: 0 | 1 | 2
readonly url: string
readonly withCredentials: boolean

View file

@ -33,6 +33,7 @@ export class BodyMixin {
readonly arrayBuffer: () => Promise<ArrayBuffer>
readonly blob: () => Promise<Blob>
readonly bytes: () => Promise<Uint8Array>
/**
* @deprecated This method is not recommended for parsing multipart/form-data bodies in server environments.
* It is recommended to use a library such as [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy) as follows:

View file

@ -2,7 +2,7 @@ import Dispatcher from './dispatcher'
export declare class RedirectHandler implements Dispatcher.DispatchHandler {
constructor (
dispatch: Dispatcher,
dispatch: Dispatcher.Dispatch,
maxRedirections: number,
opts: Dispatcher.DispatchOptions,
handler: Dispatcher.DispatchHandler,

View file

@ -14,6 +14,8 @@ declare class MockClient extends Client implements Interceptable {
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandler): boolean
/** Closes the mock client and gracefully waits for enqueued requests to complete. */
close (): Promise<void>
/** Clean up all the prepared mocks. */
cleanMocks (): void
}
declare namespace MockClient {

View file

@ -84,6 +84,8 @@ declare namespace MockInterceptor {
interface Interceptable extends Dispatcher {
/** Intercepts any matching requests that use the same origin as this mock client. */
intercept(options: MockInterceptor.Options): MockInterceptor;
/** Clean up all the prepared mocks. */
cleanMocks (): void
}
export {

View file

@ -14,6 +14,8 @@ declare class MockPool extends Pool implements Interceptable {
dispatch (options: Dispatcher.DispatchOptions, handlers: Dispatcher.DispatchHandler): boolean
/** Closes the mock pool and gracefully waits for enqueued requests to complete. */
close (): Promise<void>
/** Clean up all the prepared mocks. */
cleanMocks (): void
}
declare namespace MockPool {

View file

@ -35,6 +35,15 @@ declare namespace RetryHandler {
) => void
export interface RetryOptions {
/**
* If true, the retry handler will throw an error if the request fails,
* this will prevent the folling handlers from being called, and will destroy the socket.
*
* @type {boolean}
* @memberof RetryOptions
* @default true
*/
throwOnError?: boolean;
/**
* Callback to be invoked on every retry iteration.
* It receives the error, current state of the retry object and the options object

View file

@ -16,9 +16,12 @@ interface ConvertToIntOpts {
}
interface WebidlErrors {
/**
* @description Instantiate an error
*/
exception (opts: { header: string, message: string }): TypeError
/**
* @description Throw an error when conversion from one type to another has failed
* @description Instantiate an error when conversion from one type to another has failed
*/
conversionFailed (opts: {
prefix: string
@ -75,7 +78,7 @@ interface WebidlUtil {
): number
/**
* @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint
* @see https://webidl.spec.whatwg.org/#abstract-opdef-integerpart
*/
IntegerPart (N: number): number
@ -182,20 +185,21 @@ interface WebidlConverters {
[Key: string]: (...args: any[]) => unknown
}
type IsAssertion<T> = (arg: any) => arg is T
type WebidlIsFunction<T> = (arg: any) => arg is T
interface WebidlIs {
Request: IsAssertion<undici.Request>
Response: IsAssertion<undici.Response>
ReadableStream: IsAssertion<ReadableStream>
Blob: IsAssertion<Blob>
URLSearchParams: IsAssertion<URLSearchParams>
File: IsAssertion<File>
FormData: IsAssertion<undici.FormData>
URL: IsAssertion<URL>
WebSocketError: IsAssertion<undici.WebSocketError>
AbortSignal: IsAssertion<AbortSignal>
MessagePort: IsAssertion<MessagePort>
Request: WebidlIsFunction<undici.Request>
Response: WebidlIsFunction<undici.Response>
ReadableStream: WebidlIsFunction<ReadableStream>
Blob: WebidlIsFunction<Blob>
URLSearchParams: WebidlIsFunction<URLSearchParams>
File: WebidlIsFunction<File>
FormData: WebidlIsFunction<undici.FormData>
URL: WebidlIsFunction<URL>
WebSocketError: WebidlIsFunction<undici.WebSocketError>
AbortSignal: WebidlIsFunction<AbortSignal>
MessagePort: WebidlIsFunction<MessagePort>
USVString: WebidlIsFunction<string>
}
export interface Webidl {
@ -233,7 +237,7 @@ export interface Webidl {
* Similar to {@link Webidl.brandCheck} but allows skipping the check if third party
* interfaces are allowed.
*/
interfaceConverter <Interface>(typeCheck: IsAssertion<Interface>, name: string): (
interfaceConverter <Interface>(typeCheck: WebidlIsFunction<Interface>, name: string): (
V: unknown,
prefix: string,
argument: string

View file

@ -136,7 +136,7 @@ interface ErrorEvent extends Event {
readonly filename: string
readonly lineno: number
readonly colno: number
readonly error: any
readonly error: Error
}
export declare const ErrorEvent: {

224
deps/undici/undici.js vendored
View file

@ -543,14 +543,12 @@ var require_timers = __commonJS({
}
__name(onTick, "onTick");
function refreshTimeout() {
if (fastNowTimeout) {
if (fastNowTimeout?.refresh) {
fastNowTimeout.refresh();
} else {
clearTimeout(fastNowTimeout);
fastNowTimeout = setTimeout(onTick, TICK_MS);
if (fastNowTimeout.unref) {
fastNowTimeout.unref();
}
fastNowTimeout?.unref();
}
}
__name(refreshTimeout, "refreshTimeout");
@ -1022,7 +1020,6 @@ var require_util = __commonJS({
var stream = require("node:stream");
var net = require("node:net");
var { Blob: Blob2 } = require("node:buffer");
var nodeUtil = require("node:util");
var { stringify } = require("node:querystring");
var { EventEmitter: EE } = require("node:events");
var timers = require_timers();
@ -1411,20 +1408,6 @@ var require_util = __commonJS({
return () => signal.removeListener("abort", listener);
}
__name(addAbortListener, "addAbortListener");
var toUSVString = (() => {
if (typeof String.prototype.toWellFormed === "function") {
return (value) => `${value}`.toWellFormed();
} else {
return nodeUtil.toUSVString;
}
})();
var isUSVString = (() => {
if (typeof String.prototype.isWellFormed === "function") {
return (value) => `${value}`.isWellFormed();
} else {
return (value) => toUSVString(value) === `${value}`;
}
})();
function isTokenCharCode(c) {
switch (c) {
case 34:
@ -1575,8 +1558,6 @@ var require_util = __commonJS({
module2.exports = {
kEnumerableProperty,
isDisturbed,
toUSVString,
isUSVString,
isBlobLike,
parseOrigin,
parseURL,
@ -2147,6 +2128,8 @@ var require_diagnostics = __commonJS({
// Request
create: diagnosticsChannel.channel("undici:request:create"),
bodySent: diagnosticsChannel.channel("undici:request:bodySent"),
bodyChunkSent: diagnosticsChannel.channel("undici:request:bodyChunkSent"),
bodyChunkReceived: diagnosticsChannel.channel("undici:request:bodyChunkReceived"),
headers: diagnosticsChannel.channel("undici:request:headers"),
trailers: diagnosticsChannel.channel("undici:request:trailers"),
error: diagnosticsChannel.channel("undici:request:error"),
@ -2216,7 +2199,7 @@ var require_diagnostics = __commonJS({
const {
request: { method, path, origin }
} = evt;
debugLog("sending request to %s %s/%s", method, origin, path);
debugLog("sending request to %s %s%s", method, origin, path);
}
);
}
@ -2235,7 +2218,7 @@ var require_diagnostics = __commonJS({
response: { statusCode }
} = evt;
debugLog(
"received response to %s %s/%s - HTTP %d",
"received response to %s %s%s - HTTP %d",
method,
origin,
path,
@ -2249,7 +2232,7 @@ var require_diagnostics = __commonJS({
const {
request: { method, path, origin }
} = evt;
debugLog("trailers received from %s %s/%s", method, origin, path);
debugLog("trailers received from %s %s%s", method, origin, path);
}
);
diagnosticsChannel.subscribe(
@ -2260,7 +2243,7 @@ var require_diagnostics = __commonJS({
error
} = evt;
debugLog(
"request to %s %s/%s errored - %s",
"request to %s %s%s errored - %s",
method,
origin,
path,
@ -2489,6 +2472,9 @@ var require_request = __commonJS({
}
}
onBodySent(chunk) {
if (channels.bodyChunkSent.hasSubscribers) {
channels.bodyChunkSent.publish({ request: this, chunk });
}
if (this[kHandler].onBodySent) {
try {
return this[kHandler].onBodySent(chunk);
@ -2537,6 +2523,9 @@ var require_request = __commonJS({
onData(chunk) {
assert(!this.aborted);
assert(!this.completed);
if (channels.bodyChunkReceived.hasSubscribers) {
channels.bodyChunkReceived.publish({ request: this, chunk });
}
try {
return this[kHandler].onData(chunk);
} catch (err) {
@ -2671,61 +2660,35 @@ var require_connect = __commonJS({
var util = require_util();
var { InvalidArgumentError } = require_errors();
var tls;
var SessionCache;
if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
SessionCache = class WeakSessionCache {
static {
__name(this, "WeakSessionCache");
}
constructor(maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions;
this._sessionCache = /* @__PURE__ */ new Map();
this._sessionRegistry = new global.FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return;
}
const ref = this._sessionCache.get(key);
if (ref !== void 0 && ref.deref() === void 0) {
this._sessionCache.delete(key);
}
});
}
get(sessionKey) {
const ref = this._sessionCache.get(sessionKey);
return ref ? ref.deref() : null;
}
set(sessionKey, session) {
if (this._maxCachedSessions === 0) {
var SessionCache = class WeakSessionCache {
static {
__name(this, "WeakSessionCache");
}
constructor(maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions;
this._sessionCache = /* @__PURE__ */ new Map();
this._sessionRegistry = new FinalizationRegistry((key) => {
if (this._sessionCache.size < this._maxCachedSessions) {
return;
}
this._sessionCache.set(sessionKey, new WeakRef(session));
this._sessionRegistry.register(session, sessionKey);
}
};
} else {
SessionCache = class SimpleSessionCache {
static {
__name(this, "SimpleSessionCache");
}
constructor(maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions;
this._sessionCache = /* @__PURE__ */ new Map();
}
get(sessionKey) {
return this._sessionCache.get(sessionKey);
}
set(sessionKey, session) {
if (this._maxCachedSessions === 0) {
return;
const ref = this._sessionCache.get(key);
if (ref !== void 0 && ref.deref() === void 0) {
this._sessionCache.delete(key);
}
if (this._sessionCache.size >= this._maxCachedSessions) {
const { value: oldestKey } = this._sessionCache.keys().next();
this._sessionCache.delete(oldestKey);
}
this._sessionCache.set(sessionKey, session);
});
}
get(sessionKey) {
const ref = this._sessionCache.get(sessionKey);
return ref ? ref.deref() : null;
}
set(sessionKey, session) {
if (this._maxCachedSessions === 0) {
return;
}
};
}
this._sessionCache.set(sessionKey, new WeakRef(session));
this._sessionRegistry.register(session, sessionKey);
}
};
function buildConnector({ allowH2, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero");
@ -4072,13 +4035,12 @@ var require_data_url = __commonJS({
}
});
// lib/web/fetch/webidl.js
// lib/web/webidl/index.js
var require_webidl = __commonJS({
"lib/web/fetch/webidl.js"(exports2, module2) {
"lib/web/webidl/index.js"(exports2, module2) {
"use strict";
var { types, inspect } = require("node:util");
var { markAsUncloneable } = require("node:worker_threads");
var { toUSVString } = require_util();
var UNDEFINED = 1;
var BOOLEAN = 2;
var STRING = 3;
@ -4097,11 +4059,11 @@ var require_webidl = __commonJS({
webidl.errors.exception = function(message) {
return new TypeError(`${message.header}: ${message.message}`);
};
webidl.errors.conversionFailed = function(context) {
const plural = context.types.length === 1 ? "" : " one of";
const message = `${context.argument} could not be converted to${plural}: ${context.types.join(", ")}.`;
webidl.errors.conversionFailed = function(opts) {
const plural = opts.types.length === 1 ? "" : " one of";
const message = `${opts.argument} could not be converted to${plural}: ${opts.types.join(", ")}.`;
return webidl.errors.exception({
header: context.prefix,
header: opts.prefix,
message
});
};
@ -4272,6 +4234,8 @@ var require_webidl = __commonJS({
return inspect(V);
case STRING:
return `"${V}"`;
case BIGINT:
return `${V}n`;
default:
return `${V}`;
}
@ -4391,6 +4355,9 @@ var require_webidl = __commonJS({
return converter(V, prefix, argument);
};
};
webidl.is.USVString = function(value) {
return typeof value === "string" && value.isWellFormed();
};
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream);
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob);
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams);
@ -4427,7 +4394,12 @@ var require_webidl = __commonJS({
}
return x;
};
webidl.converters.USVString = toUSVString;
webidl.converters.USVString = function(value) {
if (typeof value === "string") {
return value.toWellFormed();
}
return `${value}`.toWellFormed();
};
webidl.converters.boolean = function(V) {
const x = Boolean(V);
return x;
@ -5202,7 +5174,7 @@ var require_util2 = __commonJS({
return;
}
if (!isUint8Array(chunk)) {
failureSteps(TypeError("Received non-Uint8Array chunk"));
failureSteps(new TypeError("Received non-Uint8Array chunk"));
return;
}
bytes.push(chunk);
@ -5674,7 +5646,7 @@ var require_formdata = __commonJS({
var require_formdata_parser = __commonJS({
"lib/web/fetch/formdata-parser.js"(exports2, module2) {
"use strict";
var { isUSVString, bufferToLowerCasedHeaderName } = require_util();
var { bufferToLowerCasedHeaderName } = require_util();
var { utf8DecodeBytes } = require_util2();
var { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require_data_url();
var { makeEntry } = require_formdata();
@ -5771,8 +5743,8 @@ var require_formdata_parser = __commonJS({
} else {
value = utf8DecodeBytes(Buffer.from(body));
}
assert(isUSVString(name));
assert(typeof value === "string" && isUSVString(value) || webidl.is.File(value));
assert(webidl.is.USVString(name));
assert(typeof value === "string" && webidl.is.USVString(value) || webidl.is.File(value));
entryList.push(makeEntry(name, value, filename));
}
}
@ -5981,7 +5953,7 @@ var require_body = __commonJS({
function noop() {
}
__name(noop, "noop");
var hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf("v18") !== 0;
var hasFinalizationRegistry = globalThis.FinalizationRegistry;
var streamRegistry;
if (hasFinalizationRegistry) {
streamRegistry = new FinalizationRegistry((weakRef) => {
@ -6480,7 +6452,7 @@ var require_client_h1 = __commonJS({
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this));
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this));
this.timeout.unref();
this.timeout?.unref();
}
}
this.timeoutValue = delay;
@ -8950,7 +8922,8 @@ var require_proxy_agent = __commonJS({
signal: opts2.signal,
headers: {
...this[kProxyHeaders],
host: opts2.host
host: opts2.host,
...opts2.connections == null || opts2.connections > 0 ? { "proxy-connection": "keep-alive" } : {}
},
servername: this[kProxyTls]?.servername || proxyHostname
});
@ -10095,45 +10068,7 @@ var require_response = __commonJS({
var require_dispatcher_weakref = __commonJS({
"lib/web/fetch/dispatcher-weakref.js"(exports2, module2) {
"use strict";
var { kConnected, kSize } = require_symbols();
var CompatWeakRef = class {
static {
__name(this, "CompatWeakRef");
}
constructor(value) {
this.value = value;
}
deref() {
return this.value[kConnected] === 0 && this.value[kSize] === 0 ? void 0 : this.value;
}
};
var CompatFinalizer = class {
static {
__name(this, "CompatFinalizer");
}
constructor(finalizer) {
this.finalizer = finalizer;
}
register(dispatcher, key) {
if (dispatcher.on) {
dispatcher.on("disconnect", () => {
if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) {
this.finalizer(key);
}
});
}
}
unregister(key) {
}
};
module2.exports = function() {
if (process.env.NODE_V8_COVERAGE && process.version.startsWith("v18")) {
process._rawDebug("Using compatibility WeakRef and FinalizationRegistry");
return {
WeakRef: CompatWeakRef,
FinalizationRegistry: CompatFinalizer
};
}
return { WeakRef, FinalizationRegistry };
};
}
@ -11903,6 +11838,11 @@ var require_fetch = __commonJS({
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}));
} else if (coding === "zstd" && typeof zlib.createZstdDecompress === "function") {
decoders.push(zlib.createZstdDecompress({
flush: zlib.constants.ZSTD_e_continue,
finishFlush: zlib.constants.ZSTD_e_end
}));
} else {
decoders.length = 0;
break;
@ -12607,7 +12547,9 @@ var require_frame = __commonJS({
}
};
module2.exports = {
WebsocketFrameSend
WebsocketFrameSend,
generateMask
// for benchmark
};
}
});
@ -12664,7 +12606,7 @@ var require_connection = __commonJS({
handler.readyState = states.CLOSED;
}
if (response.type === "error" || response.status !== 101) {
failWebsocketConnection(handler, 1002, "Received network error or non-101 status code.");
failWebsocketConnection(handler, 1002, "Received network error or non-101 status code.", response.error);
return;
}
if (protocols.length !== 0 && !response.headersList.get("Sec-WebSocket-Protocol")) {
@ -12753,7 +12695,7 @@ var require_connection = __commonJS({
}
}
__name(closeWebSocketConnection, "closeWebSocketConnection");
function failWebsocketConnection(handler, code, reason) {
function failWebsocketConnection(handler, code, reason, cause) {
if (isEstablished(handler.readyState)) {
closeWebSocketConnection(handler, code, reason, false);
}
@ -12761,7 +12703,7 @@ var require_connection = __commonJS({
if (handler.socket?.destroyed === false) {
handler.socket.destroy();
}
handler.onFail(code, reason);
handler.onFail(code, reason, cause);
}
__name(failWebsocketConnection, "failWebsocketConnection");
module2.exports = {
@ -13290,7 +13232,7 @@ var require_websocket = __commonJS({
/** @type {Handler} */
#handler = {
onConnectionEstablished: /* @__PURE__ */ __name((response, extensions) => this.#onConnectionEstablished(response, extensions), "onConnectionEstablished"),
onFail: /* @__PURE__ */ __name((code, reason) => this.#onFail(code, reason), "onFail"),
onFail: /* @__PURE__ */ __name((code, reason, cause) => this.#onFail(code, reason, cause), "onFail"),
onMessage: /* @__PURE__ */ __name((opcode, data) => this.#onMessage(opcode, data), "onMessage"),
onParserError: /* @__PURE__ */ __name((err) => failWebsocketConnection(this.#handler, null, err.message), "onParserError"),
onParserDrain: /* @__PURE__ */ __name(() => this.#onParserDrain(), "onParserDrain"),
@ -13525,10 +13467,10 @@ var require_websocket = __commonJS({
}
fireEvent("open", this);
}
#onFail(code, reason) {
#onFail(code, reason, cause) {
if (reason) {
fireEvent("error", this, (type, init) => new ErrorEvent2(type, init), {
error: new Error(reason),
error: new Error(reason, cause ? { cause } : void 0),
message: reason
});
}
@ -13698,7 +13640,7 @@ var require_util4 = __commonJS({
__name(isASCIINumber, "isASCIINumber");
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms).unref();
setTimeout(resolve, ms);
});
}
__name(delay, "delay");
@ -14074,11 +14016,9 @@ var require_eventsource = __commonJS({
dispatcher: this.#dispatcher
};
const processEventSourceEndOfBody = /* @__PURE__ */ __name((response) => {
if (isNetworkError(response)) {
this.dispatchEvent(new Event("error"));
this.close();
if (!isNetworkError(response)) {
return this.#reconnect();
}
this.#reconnect();
}, "processEventSourceEndOfBody");
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody;
fetchParams.processResponse = (response) => {
@ -14986,7 +14926,7 @@ var require_api_stream = __commonJS({
finished(res, { readable: false }, (err) => {
const { callback, res: res2, opaque: opaque2, trailers, abort } = this;
this.res = null;
if (err || !res2.readable) {
if (err || !res2?.readable) {
util.destroy(res2, err);
}
this.callback = null;

View file

@ -2,5 +2,5 @@
// Refer to tools/dep_updaters/update-undici.sh
#ifndef SRC_UNDICI_VERSION_H_
#define SRC_UNDICI_VERSION_H_
#define UNDICI_VERSION "7.10.0"
#define UNDICI_VERSION "7.11.0"
#endif // SRC_UNDICI_VERSION_H_

View file

@ -23,15 +23,16 @@ const { createProxyServer, checkProxiedRequest } = require('../common/proxy-serv
const serverHost = `localhost:${server.address().port}`;
// FIXME(undici:4083): undici currently always tunnels the request over
// CONNECT, no matter it's HTTP traffic or not, which is different from e.g.
// how curl behaves.
// CONNECT if proxyTunnel is not explicitly set to false, but what we
// need is for it to be automatically false for HTTP requests to be
// consistent with curl.
const expectedLogs = [{
method: 'CONNECT',
url: serverHost,
headers: {
// FIXME(undici:4086): this should be keep-alive.
connection: 'close',
host: serverHost
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive'
}
}];

View file

@ -33,9 +33,9 @@ const { createProxyServer, checkProxiedRequest } = require('../common/proxy-serv
method: 'CONNECT',
url: serverHost,
headers: {
// FIXME(undici:4086): this should be keep-alive.
connection: 'close',
host: serverHost
'connection': 'close',
'host': serverHost,
'proxy-connection': 'keep-alive'
}
}];