diff --git a/deps/undici/src/docs/docs/GettingStarted.md b/deps/undici/src/docs/docs/GettingStarted.md new file mode 100644 index 00000000000000..829a5c9f5809d9 --- /dev/null +++ b/deps/undici/src/docs/docs/GettingStarted.md @@ -0,0 +1,278 @@ +# Getting Started + +## Installation + +```bash +npm install undici +``` + +## Fetch + +The quickest way to get started is with `fetch`, which follows the +[Fetch Standard](https://fetch.spec.whatwg.org/) and works the same way as +the browser API: + +```js +import { fetch } from 'undici' + +const res = await fetch('https://example.com') +const data = await res.json() +console.log(data) +``` + +### Using the Request object + +undici also exports a `Request` class that follows the Fetch Standard: + +```js +import { fetch, Request } from 'undici' + +const req = new Request('https://example.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ hello: 'world' }) +}) +const res = await fetch(req) +console.log(res.status) +``` + +### Streaming the response + +`res.body` is a web `ReadableStream`. Use `pipeline` from +`node:stream/promises` to stream it to a file: + +```js +import { fetch } from 'undici' +import { pipeline } from 'node:stream/promises' +import { createWriteStream } from 'node:fs' + +const res = await fetch('https://example.com/large-file.zip') +await pipeline(res.body, createWriteStream('./file.zip')) +``` + +> Always consume or cancel the response body. In Node.js, garbage collection +> is not aggressive enough to release connections promptly, so leaving a body +> unread can cause connection leaks and stalled requests. See +> [Specification Compliance - Garbage Collection](/docs/#garbage-collection) +> for details. + +For more on `fetch`, see [API Reference: Fetch](/docs/docs/api/Fetch.md). + +## Dispatchers: Connection reuse and pooling + +By default, `fetch`, `request`, `stream`, and `pipeline` create a new connection +for each call. For applications that make many requests to the same origin, +this is wasteful. undici provides **dispatchers** that manage connections +internally. + +### `Agent` — for requests to multiple origins + +`Agent` is the most general-purpose dispatcher. It pools connections per-origin +and is the recommended default for most applications. Use it with +`setGlobalDispatcher` to affect all undici calls globally: + +```js +import { Agent, setGlobalDispatcher, fetch } from 'undici' + +const agent = new Agent({ + keepAliveTimeout: 30_000, + keepAliveMaxTimeout: 600_000 +}) +setGlobalDispatcher(agent) + +// All subsequent fetch/request/stream/pipeline calls reuse connections +const res = await fetch('https://api.example.com/data') +``` + +You can also pass a dispatcher per-request: + +```js +await fetch('https://api.example.com/data', { dispatcher: agent }) +``` + +### `Pool` — for requests to a single origin + +`Pool` manages a fixed set of connections to one origin. It gives you explicit +control over concurrency: + +```js +import { Pool, request } from 'undici' + +const pool = new Pool('https://api.example.com', { connections: 10 }) + +const { body } = await request('https://api.example.com/data', { + dispatcher: pool +}) +const data = await body.json() + +pool.close() +``` + +### `Client` — for a single connection + +`Client` maps to a single TCP connection. It supports pipelining (sending +multiple requests before responses arrive): + +```js +import { Client } from 'undici' + +const client = new Client('https://api.example.com', { + pipelining: 5 +}) + +const { body } = await client.request({ path: '/', method: 'GET' }) +await body.dump() + +client.close() +``` + +For more on dispatcher options and lifecycle, see: +- [API Reference: Agent](/docs/docs/api/Agent.md) +- [API Reference: Pool](/docs/docs/api/Pool.md) +- [API Reference: Client](/docs/docs/api/Client.md) + +## Timeouts + +undici applies timeouts at two levels: + +- **`headersTimeout`** — time to wait for response headers (default: 300s). +- **`bodyTimeout`** — time between consecutive body chunks (default: 300s). + +Set these on the dispatcher or per-request: + +```js +import { Agent, setGlobalDispatcher } from 'undici' + +const agent = new Agent({ + headersTimeout: 5_000, + bodyTimeout: 30_000 +}) + +setGlobalDispatcher(agent) +``` + +Timeout errors are thrown as `HeadersTimeoutError` and `BodyTimeoutError`. +See [API Reference: Errors](/docs/docs/api/Errors.md) for the full list. + +## Error handling + +undici exposes structured errors via `error.code`: + +```js +import { request, errors } from 'undici' + +try { + const { body } = await request('https://example.com') + await body.json() +} catch (err) { + switch (err.code) { + case 'UND_ERR_CONNECT_TIMEOUT': + console.error('Connection timed out') + break + case 'UND_ERR_HEADERS_TIMEOUT': + console.error('Headers timed out') + break + case 'UND_ERR_BODY_TIMEOUT': + console.error('Body timed out') + break + case 'UND_ERR_ABORTED': + console.error('Request was aborted') + break + default: + console.error(err) + } +} +``` + +### Aborting requests + +```js +import { request } from 'undici' + +const ac = new AbortController() + +setTimeout(() => ac.abort(), 1000) + +try { + const { body } = await request('https://example.com', { + signal: ac.signal + }) + await body.dump() +} catch (err) { + console.error(err.code) // UND_ERR_ABORTED +} +``` + +## Common patterns + +### Proxies + +Use `ProxyAgent` for HTTP(S) proxies, or `EnvHttpProxyAgent` to pick up +proxy settings from environment variables: + +```js +import { ProxyAgent, setGlobalDispatcher } from 'undici' + +const proxy = new ProxyAgent('http://proxy.internal:8080') +setGlobalDispatcher(proxy) +``` + +See [Best Practices: Proxy](/docs/docs/best-practices/proxy.md) and +[API Reference: ProxyAgent](/docs/docs/api/ProxyAgent.md). + +### Mocking in tests + +```js +import { MockAgent, setGlobalDispatcher, request } from 'undici' + +const mockAgent = new MockAgent() +setGlobalDispatcher(mockAgent) + +const mockPool = mockAgent.get('https://api.example.com') +mockPool.intercept({ path: '/users' }).reply(200, [{ id: 1 }]) + +const { body } = await request('https://api.example.com/users') +console.log(await body.json()) +``` + +See [Best Practices: Mocking Request](/docs/docs/best-practices/mocking-request.md) +and [API Reference: MockAgent](/docs/docs/api/MockAgent.md). + +### Testing with undici + +For test suites, set short keep-alive timeouts to avoid slow teardowns: + +```js +import { Agent, setGlobalDispatcher } from 'undici' + +const agent = new Agent({ + keepAliveTimeout: 10, + keepAliveMaxTimeout: 10 +}) +setGlobalDispatcher(agent) +``` + +See [Best Practices: Writing Tests](/docs/docs/best-practices/writing-tests.md). + +### Customizing the global fetch + +You can override Node.js's built-in globals with `install()`: + +```js +import { install } from 'undici' + +install() + +// Global fetch, Headers, Response, Request, and FormData +// now come from undici, not the Node.js bundle +const res = await fetch('https://example.com') +``` + +See [API Reference: Global Installation](/docs/docs/api/GlobalInstallation.md). + +## Further reading + +- [Undici vs. Built-in Fetch](/docs/docs/best-practices/undici-vs-builtin-fetch.md) — + when to install undici vs using Node.js built-in fetch +- [API Reference](/docs/docs/api/Dispatcher.md) — full dispatcher API documentation +- [Examples](/docs/examples/) — runnable code examples diff --git a/deps/undici/src/docs/docs/api/BalancedPool.md b/deps/undici/src/docs/docs/api/BalancedPool.md index df267fe727054a..6d80317af781a3 100644 --- a/deps/undici/src/docs/docs/api/BalancedPool.md +++ b/deps/undici/src/docs/docs/api/BalancedPool.md @@ -34,7 +34,7 @@ Implements [Client.closed](/docs/docs/api/Client.md#clientclosed) Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) -### `Pool.stats` +### `BalancedPool.stats` Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool. diff --git a/deps/undici/src/docs/docs/api/Cookies.md b/deps/undici/src/docs/docs/api/Cookies.md index 0cad37914d6258..9c07eb46c4e7c5 100644 --- a/deps/undici/src/docs/docs/api/Cookies.md +++ b/deps/undici/src/docs/docs/api/Cookies.md @@ -10,7 +10,7 @@ * **path** `string` (optional) * **secure** `boolean` (optional) * **httpOnly** `boolean` (optional) -* **sameSite** `'String'|'Lax'|'None'` (optional) +* **sameSite** `'Strict'|'Lax'|'None'` (optional) * **unparsed** `string[]` (optional) Left over attributes that weren't parsed. ## `deleteCookie(headers, name[, attributes])` diff --git a/deps/undici/src/docs/docs/api/Dispatcher.md b/deps/undici/src/docs/docs/api/Dispatcher.md index 2137e174a8ae74..9553095bd74443 100644 --- a/deps/undici/src/docs/docs/api/Dispatcher.md +++ b/deps/undici/src/docs/docs/api/Dispatcher.md @@ -211,6 +211,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests. * **onResponseEnd** `(controller: DispatchController, trailers: Record) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests. * **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw. +* **onBodySent** `(chunk: Buffer) => void` (optional) - Invoked when a chunk of the request body is sent. #### Migration from legacy handler API @@ -688,7 +689,7 @@ return null A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream. -As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatch.md#example-2-stream-to-fastify-response) for more details. +As demonstrated in [Example 1 - Basic GET stream request](/docs/docs/api/Dispatcher.md#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](/docs/docs/api/Dispatcher.md#example-2-stream-to-fastify-response) for more details. Arguments: @@ -1016,7 +1017,7 @@ The `retry` interceptor allows you to customize the way your dispatcher handles It accepts the same arguments as the [`RetryHandler` constructor](/docs/docs/api/RetryHandler.md). -**Example - Basic Redirect Interceptor** +**Example - Basic Retry Interceptor** ```js const { Client, interceptors } = require("undici"); @@ -1112,7 +1113,7 @@ It represents a storage object for resolved DNS records. **Example - Basic DNS Interceptor** ```js -const { Client, interceptors } = require("undici"); +const { Agent, interceptors } = require("undici"); const { dns } = interceptors; const client = new Agent().compose([ @@ -1128,7 +1129,7 @@ const response = await client.request({ **Example - DNS Interceptor and LRU cache as a storage** ```js -const { Client, interceptors } = require("undici"); +const { Agent, interceptors } = require("undici"); const QuickLRU = require("quick-lru"); const { dns } = interceptors; diff --git a/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md b/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md index adc2a24245762d..ccc16e819a2a14 100644 --- a/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md +++ b/deps/undici/src/docs/docs/api/EnvHttpProxyAgent.md @@ -52,11 +52,11 @@ import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici' const envHttpProxyAgent = new EnvHttpProxyAgent() setGlobalDispatcher(envHttpProxyAgent) -const { status, json } = await fetch('http://localhost:3000/foo') +const response = await fetch('http://localhost:3000/foo') -console.log('response received', status) // response received 200 +console.log('response received', response.status) // response received 200 -const data = await json() // data { foo: "bar" } +const data = await response.json() // data { foo: "bar" } ``` #### Example - Basic Proxy Request with global agent dispatcher @@ -102,14 +102,11 @@ import { EnvHttpProxyAgent, fetch } from 'undici' const envHttpProxyAgent = new EnvHttpProxyAgent() -const { - status, - json -} = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) +const response = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent }) -console.log('response received', status) // response received 200 +console.log('response received', response.status) // response received 200 -const data = await json() // data { foo: "bar" } +const data = await response.json() // data { foo: "bar" } ``` ## Instance Methods diff --git a/deps/undici/src/docs/docs/api/Errors.md b/deps/undici/src/docs/docs/api/Errors.md index 58e29790fe455a..6a4bc3c6706be2 100644 --- a/deps/undici/src/docs/docs/api/Errors.md +++ b/deps/undici/src/docs/docs/api/Errors.md @@ -32,7 +32,7 @@ import { errors } from 'undici' | `ResponseError` | `UND_ERR_RESPONSE` | response returned an error status code; carries `statusCode`, `headers` and `body`. | | `MaxOriginsReachedError` | `UND_ERR_MAX_ORIGINS_REACHED` | the maximum number of allowed origins has been reached. | | `BalancedPoolMissingUpstreamError` | `UND_ERR_BPL_MISSING_UPSTREAM` | no upstream has been added to the `BalancedPool`. | -| `Socks5ProxyError` | `UND_ERR_SOCKS5*` | an error occurred during SOCKS5 proxy negotiation. | +| `Socks5ProxyError` | `UND_ERR_SOCKS5` | an error occurred during SOCKS5 proxy negotiation. | | `HTTPParserError` | `HPE_*` | an error occurred while parsing the HTTP response (extends `Error`, not `UndiciError`). | Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === ''` instead to avoid inconsistencies. diff --git a/deps/undici/src/docs/docs/api/Fetch.md b/deps/undici/src/docs/docs/api/Fetch.md index 8588dbaac6599a..1e0c962fbc3260 100644 --- a/deps/undici/src/docs/docs/api/Fetch.md +++ b/deps/undici/src/docs/docs/api/Fetch.md @@ -15,7 +15,7 @@ same implementation. Use the built-in global `FormData` with the built-in global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`. If you want the installed `undici` package to provide the globals, call -[`install()`](/docs/api/GlobalInstallation.md) so `fetch`, `Headers`, +[`install()`](/docs/docs/api/GlobalInstallation.md) so `fetch`, `Headers`, `Response`, `Request`, and `FormData` are installed together as a matching set. ## Response @@ -26,7 +26,7 @@ This API is implemented as per the standard, you can find documentation on [MDN] This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request) -## Header +## Headers This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers) diff --git a/deps/undici/src/docs/docs/api/H2CClient.md b/deps/undici/src/docs/docs/api/H2CClient.md index 2c21e5bd717fed..2c96ae921f5126 100644 --- a/deps/undici/src/docs/docs/api/H2CClient.md +++ b/deps/undici/src/docs/docs/api/H2CClient.md @@ -17,7 +17,7 @@ const server = createServer((req, res) => { }) server.listen() -once(server, 'listening').then(() => { +once(server, 'listening').then(async () => { const client = new H2CClient(`http://localhost:${server.address().port}/`) const response = await client.request({ path: '/', method: 'GET' }) diff --git a/deps/undici/src/docs/docs/api/MockAgent.md b/deps/undici/src/docs/docs/api/MockAgent.md index b4ce8106bb0ef4..d46f1b95a21cb1 100644 --- a/deps/undici/src/docs/docs/api/MockAgent.md +++ b/deps/undici/src/docs/docs/api/MockAgent.md @@ -580,7 +580,7 @@ mockAgent.getCallHistory()?.firstCall() ```js const mockAgent = new MockAgent() -mockAgent.clearAllCallHistory() +mockAgent.clearCallHistory() ``` #### Example - call history instance class method diff --git a/deps/undici/src/docs/docs/api/MockCallHistory.md b/deps/undici/src/docs/docs/api/MockCallHistory.md index 7473453b128f34..4fd393b941e197 100644 --- a/deps/undici/src/docs/docs/api/MockCallHistory.md +++ b/deps/undici/src/docs/docs/api/MockCallHistory.md @@ -165,7 +165,7 @@ Parameters : - criteria : the first parameter. a function, regexp or object. - function : filter MockCallHistoryLog when the function returns false - - regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](./MockCallHistoryLog.md#to-string)) + - regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](/docs/docs/api/MockCallHistoryLog.md#to-string)) - object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](/docs/docs/api/MockCallHistory.md#filter-parameter) - options : the second parameter. an object. - options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below diff --git a/deps/undici/src/docs/docs/api/Pool.md b/deps/undici/src/docs/docs/api/Pool.md index bfa1721d3109c4..1d1f7e140ef79e 100644 --- a/deps/undici/src/docs/docs/api/Pool.md +++ b/deps/undici/src/docs/docs/api/Pool.md @@ -36,7 +36,7 @@ Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) ### `Pool.stats` -Returns [`PoolStats`](PoolStats.md) instance for this pool. +Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool. ## Instance Methods diff --git a/deps/undici/src/docs/docs/api/RetryAgent.md b/deps/undici/src/docs/docs/api/RetryAgent.md index fdf394fbad483a..3dc206ce63fb50 100644 --- a/deps/undici/src/docs/docs/api/RetryAgent.md +++ b/deps/undici/src/docs/docs/api/RetryAgent.md @@ -12,7 +12,7 @@ Arguments: * **dispatcher** `undici.Dispatcher` (required) - the dispatcher to wrap * **options** `RetryHandlerOptions` (optional) - the options -Returns: `ProxyAgent` +Returns: `RetryAgent` ### Parameter: `RetryHandlerOptions` @@ -23,9 +23,9 @@ Returns: `ProxyAgent` - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second) - **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2` - **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true` -- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` +- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']` - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` -- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']` +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']` **`RetryContext`** diff --git a/deps/undici/src/docs/docs/api/RetryHandler.md b/deps/undici/src/docs/docs/api/RetryHandler.md index 07e7a2dac3359a..00ef0a9000fa04 100644 --- a/deps/undici/src/docs/docs/api/RetryHandler.md +++ b/deps/undici/src/docs/docs/api/RetryHandler.md @@ -4,12 +4,12 @@ Extends: `undici.DispatcherHandlers` A handler class that implements the retry logic for a request. -## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])` +## `new RetryHandler(opts, { dispatch, handler })` Arguments: -- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`. -- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle. +- **opts** `Dispatch.DispatchOptions & { retryOptions?: RetryOptions }` (required) - An intersection of `Dispatcher.DispatchOptions` and an optional `RetryOptions` object. +- **{ dispatch, handler }** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle. Returns: `retryHandler` @@ -20,15 +20,15 @@ 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. +- **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) - **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second) - **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2` - **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true` -- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` +- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']` - **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` -- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']` +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']` **`RetryContext`** diff --git a/deps/undici/src/docs/docs/api/RoundRobinPool.md b/deps/undici/src/docs/docs/api/RoundRobinPool.md index 7221122ae6d2cb..ea9a3e1aa54cd8 100644 --- a/deps/undici/src/docs/docs/api/RoundRobinPool.md +++ b/deps/undici/src/docs/docs/api/RoundRobinPool.md @@ -66,7 +66,7 @@ Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed) ### `RoundRobinPool.stats` -Returns [`PoolStats`](PoolStats.md) instance for this pool. +Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool. ## Instance Methods diff --git a/deps/undici/src/docs/docs/api/SnapshotAgent.md b/deps/undici/src/docs/docs/api/SnapshotAgent.md index 1de74fb73f97c3..cac2981c67c9f7 100644 --- a/deps/undici/src/docs/docs/api/SnapshotAgent.md +++ b/deps/undici/src/docs/docs/api/SnapshotAgent.md @@ -634,6 +634,6 @@ SnapshotAgent provides similar functionality to nock but is specifically designe ## See Also -- [MockAgent](./MockAgent.md) - Manual mocking for more control -- [MockCallHistory](./MockCallHistory.md) - Inspecting request history -- [Testing Best Practices](../best-practices/writing-tests.md) - General testing guidance \ No newline at end of file +- [MockAgent](/docs/docs/api/MockAgent.md) - Manual mocking for more control +- [MockCallHistory](/docs/docs/api/MockCallHistory.md) - Inspecting request history +- [Testing Best Practices](/docs/docs/best-practices/writing-tests.md) - General testing guidance \ No newline at end of file diff --git a/deps/undici/src/docs/docs/api/api-lifecycle.md b/deps/undici/src/docs/docs/api/api-lifecycle.md index ee08292cc7d37a..7abefc9b7ee874 100644 --- a/deps/undici/src/docs/docs/api/api-lifecycle.md +++ b/deps/undici/src/docs/docs/api/api-lifecycle.md @@ -58,9 +58,9 @@ stateDiagram-v2 ### idle -The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](Client.md#clientconnectoptions-callback), [`Client.pipeline()`](Client.md#clientpipelineoptions-handler), [`Client.request()`](Client.md#clientrequestoptions-callback), [`Client.stream()`](Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](/docs/docs/api/Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](/docs/docs/api/Client.md#pending) and then most likely directly to [**processing**](/docs/docs/api/Client.md#processing). +The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](/docs/docs/api/Client.md#clientconnectoptions-callback), [`Client.pipeline()`](/docs/docs/api/Client.md#clientpipelineoptions-handler), [`Client.request()`](/docs/docs/api/Client.md#clientrequestoptions-callback), [`Client.stream()`](/docs/docs/api/Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](/docs/docs/api/Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](/docs/docs/api/Client.md#pending) and then most likely directly to [**processing**](/docs/docs/api/Client.md#processing). -Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) or [`Client.destroy()`](Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state since the `Client` instance will have no queued requests in this state. +Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) or [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state since the `Client` instance will have no queued requests in this state. ### pending @@ -72,11 +72,11 @@ Calling [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callbac ### processing -The **processing** state is a state machine within itself. It initializes to the [**processing.running**](/docs/docs/api/Client.md#running) state. The [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers), [`Client.close()`](Client.md#clientclosecallback), and [`Client.destroy()`](Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](/docs/docs/api/Client.md#closing) state. And `Client.destroy()` will transition to [**destroyed**](/docs/docs/api/Client.md#destroyed). +The **processing** state is a state machine within itself. It initializes to the [**processing.running**](/docs/docs/api/Client.md#running) state. The [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers), [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback), and [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](/docs/docs/api/Client.md#closing) state. And `Client.destroy()` will transition to [**destroyed**](/docs/docs/api/Client.md#destroyed). #### running -In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](/docs/docs/api/Client.md#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](/docs/docs/api/Client.md#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) nor [`Client.destroy()`](Client.md#clientdestroyerror-callback) are called, then the [**processing**](/docs/docs/api/Client.md#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](/docs/docs/api/Client.md#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](/docs/docs/api/Client.md#idle) state. +In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](/docs/docs/api/Client.md#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](/docs/docs/api/Client.md#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) nor [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) are called, then the [**processing**](/docs/docs/api/Client.md#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](/docs/docs/api/Client.md#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](/docs/docs/api/Client.md#idle) state. #### busy diff --git a/deps/undici/src/lib/dispatcher/client-h1.js b/deps/undici/src/lib/dispatcher/client-h1.js index f1c52fb5f116ec..4184670f6738c3 100644 --- a/deps/undici/src/lib/dispatcher/client-h1.js +++ b/deps/undici/src/lib/dispatcher/client-h1.js @@ -371,7 +371,6 @@ class Parser { finish () { assert(currentParser === null) assert(this.ptr != null) - assert(!this.paused) const { llhttp } = this diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js index e378010513a139..e980c12d948d9a 100644 --- a/deps/undici/src/lib/dispatcher/client-h2.js +++ b/deps/undici/src/lib/dispatcher/client-h2.js @@ -556,6 +556,19 @@ function onUpgradeStreamClose () { } function onRequestStreamClose () { + const state = this[kRequestStreamState] + + if (state) { + // Release the stream first so request references are cleared, + // then complete the response with trailers if available. + releaseRequestStream(this) + + if (state.pendingEnd && !state.request.aborted && !state.request.completed) { + state.request.onResponseEnd(state.trailers || {}) + state.finalizeRequest() + } + } + this.off('data', onData) this.off('error', noop) closeStreamSession(this) @@ -1081,14 +1094,14 @@ function onEnd () { stream.off('end', onEnd) - releaseRequestStream(stream) - // If we received a response, this is a normal completion + // If we received a response, this is a normal completion. + // Defer actual completion to onRequestStreamClose so that + // onTrailers (which may fire after 'end' on Windows) can + // store trailers first. if (state.responseReceived) { if (!request.aborted && !request.completed) { - request.onResponseEnd({}) + state.pendingEnd = true } - - state.finalizeRequest() } else { // Stream ended without receiving a response - this is an error // (e.g., server destroyed the stream before sending headers) @@ -1101,8 +1114,6 @@ function onError (err) { const state = stream[kRequestStreamState] stream.off('error', onError) - - releaseRequestStream(stream) state.abort(err) } @@ -1111,8 +1122,6 @@ function onFrameError (type, code) { const state = stream[kRequestStreamState] stream.off('frameError', onFrameError) - - releaseRequestStream(stream) state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)) } @@ -1124,7 +1133,8 @@ function onTimeout () { const stream = this const state = stream[kRequestStreamState] - releaseRequestStream(stream) + // Remove self so timeout doesn't fire again after we handle it + stream.off('timeout', onTimeout) const err = state.responseReceived ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`) @@ -1138,14 +1148,14 @@ function onTrailers (trailers) { const { request } = state stream.off('trailers', onTrailers) + stream.off('data', onData) if (request.aborted || request.completed) { return } - releaseRequestStream(stream) - request.onResponseEnd(trailers) - state.finalizeRequest() + // Store trailers for onRequestStreamClose to use when completing + state.trailers = trailers } function writeBodyH2 () { diff --git a/deps/undici/src/lib/dispatcher/client.js b/deps/undici/src/lib/dispatcher/client.js index c6cd7d170ebdd5..37d3d139825bbb 100644 --- a/deps/undici/src/lib/dispatcher/client.js +++ b/deps/undici/src/lib/dispatcher/client.js @@ -568,9 +568,15 @@ function handleConnectError (client, err, { host, hostname, protocol, port }) { } if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { - assert(client[kRunning] === 0) + const running = client[kQueue].splice(client[kRunningIdx], client[kRunning]) + client[kPendingIdx] = client[kRunningIdx] + + for (let i = 0; i < running.length; i++) { + util.errorRequest(client, running[i], err) + } + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { - const request = client[kQueue][client[kPendingIdx]++] + const request = client[kQueue].splice(client[kPendingIdx], 1)[0] util.errorRequest(client, request, err) } } else { diff --git a/deps/undici/src/lib/interceptor/dns.js b/deps/undici/src/lib/interceptor/dns.js index 6347d1ffefc3a7..ebc9a5383036be 100644 --- a/deps/undici/src/lib/interceptor/dns.js +++ b/deps/undici/src/lib/interceptor/dns.js @@ -535,6 +535,10 @@ module.exports = interceptorOpts => { return dispatch => { return function dnsInterceptor (origDispatchOpts, handler) { + if (origDispatchOpts.origin == null) { + return dispatch(origDispatchOpts, handler) + } + const origin = origDispatchOpts.origin.constructor === URL ? origDispatchOpts.origin diff --git a/deps/undici/src/lib/llhttp/wasm_build_env.txt b/deps/undici/src/lib/llhttp/wasm_build_env.txt index a11903eea99064..9082352845120a 100644 --- a/deps/undici/src/lib/llhttp/wasm_build_env.txt +++ b/deps/undici/src/lib/llhttp/wasm_build_env.txt @@ -1,5 +1,5 @@ -> undici@8.4.0 build:wasm +> undici@8.4.1 build:wasm > node build/wasm.js --docker > docker run --rm --platform=linux/x86_64 --user 1001:1001 --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 diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json index d574145badb9f2..3d16863a78fd3e 100644 --- a/deps/undici/src/package-lock.json +++ b/deps/undici/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "undici", - "version": "8.4.0", + "version": "8.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undici", - "version": "8.4.0", + "version": "8.4.1", "license": "MIT", "devDependencies": { "@fastify/busboy": "3.2.0", diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 6fb07c18274458..16678ee67dc109 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "8.4.0", + "version": "8.4.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/undici.js b/deps/undici/undici.js index c62257ae159057..d18eefacec6220 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -7262,7 +7262,6 @@ var require_client_h1 = __commonJS({ finish() { assert(currentParser === null); assert(this.ptr != null); - assert(!this.paused); const { llhttp } = this; let ret; try { @@ -8641,6 +8640,14 @@ var require_client_h2 = __commonJS({ } __name(onUpgradeStreamClose, "onUpgradeStreamClose"); function onRequestStreamClose() { + const state = this[kRequestStreamState]; + if (state) { + releaseRequestStream(this); + if (state.pendingEnd && !state.request.aborted && !state.request.completed) { + state.request.onResponseEnd(state.trailers || {}); + state.finalizeRequest(); + } + } this.off("data", onData); this.off("error", noop); closeStreamSession(this); @@ -9028,12 +9035,10 @@ var require_client_h2 = __commonJS({ const state = stream[kRequestStreamState]; const { request } = state; stream.off("end", onEnd); - releaseRequestStream(stream); if (state.responseReceived) { if (!request.aborted && !request.completed) { - request.onResponseEnd({}); + state.pendingEnd = true; } - state.finalizeRequest(); } else { state.abort(new InformationalError("HTTP/2: stream half-closed (remote)"), true); } @@ -9043,7 +9048,6 @@ var require_client_h2 = __commonJS({ const stream = this; const state = stream[kRequestStreamState]; stream.off("error", onError); - releaseRequestStream(stream); state.abort(err); } __name(onError, "onError"); @@ -9051,7 +9055,6 @@ var require_client_h2 = __commonJS({ const stream = this; const state = stream[kRequestStreamState]; stream.off("frameError", onFrameError); - releaseRequestStream(stream); state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)); } __name(onFrameError, "onFrameError"); @@ -9062,7 +9065,7 @@ var require_client_h2 = __commonJS({ function onTimeout() { const stream = this; const state = stream[kRequestStreamState]; - releaseRequestStream(stream); + stream.off("timeout", onTimeout); const err = state.responseReceived ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`) : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`); state.abort(err); } @@ -9072,12 +9075,11 @@ var require_client_h2 = __commonJS({ const state = stream[kRequestStreamState]; const { request } = state; stream.off("trailers", onTrailers); + stream.off("data", onData); if (request.aborted || request.completed) { return; } - releaseRequestStream(stream); - request.onResponseEnd(trailers); - state.finalizeRequest(); + state.trailers = trailers; } __name(onTrailers, "onTrailers"); function writeBodyH2() { @@ -9719,9 +9721,13 @@ var require_client = __commonJS({ }); } if (err.code === "ERR_TLS_CERT_ALTNAME_INVALID") { - assert(client[kRunning] === 0); + const running = client[kQueue].splice(client[kRunningIdx], client[kRunning]); + client[kPendingIdx] = client[kRunningIdx]; + for (let i = 0; i < running.length; i++) { + util.errorRequest(client, running[i], err); + } while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { - const request = client[kQueue][client[kPendingIdx]++]; + const request = client[kQueue].splice(client[kPendingIdx], 1)[0]; util.errorRequest(client, request, err); } } else { diff --git a/src/undici_version.h b/src/undici_version.h index 438598cb5da9ac..ce66169689ab13 100644 --- a/src/undici_version.h +++ b/src/undici_version.h @@ -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 "8.4.0" +#define UNDICI_VERSION "8.4.1" #endif // SRC_UNDICI_VERSION_H_