From 159bd2a2e18cbc0bbf29ca7494aac271f285ca5d Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 15 Jun 2026 16:43:51 +0000 Subject: [PATCH] deps: update undici to 8.5.0 --- deps/undici/src/docs/docs/GettingStarted.md | 278 ++++++++++++++++++ deps/undici/src/docs/docs/api/BalancedPool.md | 2 +- deps/undici/src/docs/docs/api/Client.md | 1 + deps/undici/src/docs/docs/api/Cookies.md | 29 +- deps/undici/src/docs/docs/api/Dispatcher.md | 20 +- .../src/docs/docs/api/EnvHttpProxyAgent.md | 15 +- deps/undici/src/docs/docs/api/Errors.md | 2 +- deps/undici/src/docs/docs/api/Fetch.md | 4 +- deps/undici/src/docs/docs/api/H2CClient.md | 2 +- deps/undici/src/docs/docs/api/MockAgent.md | 2 +- .../src/docs/docs/api/MockCallHistory.md | 2 +- deps/undici/src/docs/docs/api/Pool.md | 2 +- deps/undici/src/docs/docs/api/RetryAgent.md | 6 +- deps/undici/src/docs/docs/api/RetryHandler.md | 12 +- .../src/docs/docs/api/RoundRobinPool.md | 2 +- .../undici/src/docs/docs/api/SnapshotAgent.md | 6 +- .../src/docs/docs/api/Socks5ProxyAgent.md | 1 + .../undici/src/docs/docs/api/api-lifecycle.md | 8 +- deps/undici/src/lib/dispatcher/client-h1.js | 71 ++++- deps/undici/src/lib/dispatcher/client-h2.js | 132 +++++++-- deps/undici/src/lib/dispatcher/client.js | 18 +- .../src/lib/dispatcher/dispatcher-base.js | 1 + deps/undici/src/lib/dispatcher/proxy-agent.js | 3 +- .../src/lib/dispatcher/socks5-proxy-agent.js | 6 +- deps/undici/src/lib/interceptor/dns.js | 4 + deps/undici/src/lib/llhttp/wasm_build_env.txt | 2 +- deps/undici/src/lib/util/cache.js | 10 +- deps/undici/src/lib/web/cookies/parse.js | 42 ++- .../src/lib/web/eventsource/eventsource.js | 25 +- deps/undici/src/lib/web/eventsource/util.js | 33 ++- deps/undici/src/lib/web/fetch/body.js | 43 +++ deps/undici/src/lib/web/fetch/request.js | 1 + deps/undici/src/lib/web/websocket/receiver.js | 23 +- .../web/websocket/stream/websocketstream.js | 9 +- .../undici/src/lib/web/websocket/websocket.js | 4 +- deps/undici/src/package-lock.json | 4 +- deps/undici/src/package.json | 2 +- .../src/repro-h2-pipelining-default.mjs | 78 ----- deps/undici/src/types/client.d.ts | 5 + deps/undici/src/types/fetch.d.ts | 1 + deps/undici/undici.js | 273 ++++++++++++++--- src/undici_version.h | 2 +- 42 files changed, 933 insertions(+), 253 deletions(-) create mode 100644 deps/undici/src/docs/docs/GettingStarted.md delete mode 100644 deps/undici/src/repro-h2-pipelining-default.mjs 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/Client.md b/deps/undici/src/docs/docs/api/Client.md index 726de4dbad2cba..0152f380c8fa83 100644 --- a/deps/undici/src/docs/docs/api/Client.md +++ b/deps/undici/src/docs/docs/api/Client.md @@ -25,6 +25,7 @@ Returns: `Client` * **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. + * **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit. * **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. This option has no effect once HTTP/2 is negotiated — see `maxConcurrentStreams` for the h2 dispatch ceiling. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null` - Configures how undici establishes TCP/TLS connections. Accepts two forms: diff --git a/deps/undici/src/docs/docs/api/Cookies.md b/deps/undici/src/docs/docs/api/Cookies.md index 0cad37914d6258..bd2b63f6d976f5 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])` @@ -80,6 +80,33 @@ Arguments: Returns: `Cookie[]` +## `parseCookie(cookie)` + +Parses a single `Set-Cookie` header value into a `Cookie` object. + +```js +import { parseCookie } from 'undici' + +console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax')) +// { +// name: 'undici', +// value: 'getSetCookies', +// secure: true, +// sameSite: 'Lax' +// } +``` + +Notes: + +* The cookie value is returned as it appears in the header. Percent-encoded sequences such as `%20` or `%0D%0A` are **not** decoded. +* `sameSite` is only set for exact case-insensitive matches of `Strict`, `Lax`, or `None`. + +Arguments: + +* **cookie** `string` + +Returns: `Cookie | null` + ## `setCookie(headers, cookie)` Appends a cookie to the `Set-Cookie` header. diff --git a/deps/undici/src/docs/docs/api/Dispatcher.md b/deps/undici/src/docs/docs/api/Dispatcher.md index 2137e174a8ae74..eab6fa58dfa27f 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; @@ -1375,14 +1376,23 @@ When using the array header format (`string[]`), Undici processes only indexed e Response headers will derive a `host` from the `url` of the [Client](/docs/docs/api/Client.md#class-client) instance if no `host` header was previously specified. +### Request header validation + +Request headers that are managed by the HTTP connection are handled differently from ordinary headers: + +* `transfer-encoding`, `keep-alive`, and `upgrade` cannot be set through `options.headers`; Undici throws an `InvalidArgumentError`. +* `expect` is not supported; Undici throws a `NotSupportedError`. +* `connection` must be a string containing comma-separated valid HTTP tokens. Undici rejects malformed tokens with `InvalidArgumentError: invalid connection header` and uses the `close` token to request connection reset behavior. +* `host` and `content-length` are tracked separately from the raw header list. Duplicate `host` or `content-length` values are rejected, and `content-length` must contain only decimal digits. + ### Example 1 - Object ```js { 'content-length': '123', 'content-type': 'text/plain', - connection: 'keep-alive', host: 'mysite.com', + 'accept-language': 'en', accept: '*/*' } ``` 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/Socks5ProxyAgent.md b/deps/undici/src/docs/docs/api/Socks5ProxyAgent.md index ef6fc635a8f747..3ef0f852f27a97 100644 --- a/deps/undici/src/docs/docs/api/Socks5ProxyAgent.md +++ b/deps/undici/src/docs/docs/api/Socks5ProxyAgent.md @@ -22,6 +22,7 @@ Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions) * **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL. * **connect** `Function` (optional) - Custom connector function for the proxy connection. * **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS). +* **requestTls** `BuildOptions` (optional) - TLS options applied to the HTTPS connection to the target server through the SOCKS5 tunnel. Use this to configure `ca`, `cert`, `key`, `rejectUnauthorized`, `servername`, etc. for the target HTTPS endpoint. Examples: 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..dfedeb49c7bc08 100644 --- a/deps/undici/src/lib/dispatcher/client-h1.js +++ b/deps/undici/src/lib/dispatcher/client-h1.js @@ -57,6 +57,9 @@ const constants = require('../llhttp/constants.js') const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const removeAllListeners = util.removeAllListeners +const kIdleSocketValidation = Symbol('kIdleSocketValidation') +const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') +const kSocketUsed = Symbol('kSocketUsed') let extractBody @@ -371,7 +374,6 @@ class Parser { finish () { assert(currentParser === null) assert(this.ptr != null) - assert(!this.paused) const { llhttp } = this @@ -450,6 +452,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { return -1 @@ -585,6 +592,11 @@ class Parser { return -1 } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + const request = client[kQueue][client[kRunningIdx]] if (!request) { @@ -763,6 +775,7 @@ class Parser { request.onResponseEnd(headers) client[kQueue][client[kRunningIdx]++] = null + socket[kSocketUsed] = client[kPending] === 0 if (socket[kWriting]) { assert(client[kRunning] === 0) @@ -839,6 +852,9 @@ function connectH1 (client, socket) { socket[kWriting] = false socket[kReset] = false socket[kBlocking] = false + socket[kIdleSocketValidation] = 0 + socket[kIdleSocketValidationTimeout] = null + socket[kSocketUsed] = false socket[kParser] = new Parser(client, socket, llhttpInstance) util.addListener(socket, 'error', onHttpSocketError) @@ -881,7 +897,7 @@ function connectH1 (client, socket) { * @returns {boolean} */ busy (request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true } @@ -961,6 +977,8 @@ function onHttpSocketEnd () { function onHttpSocketClose () { const parser = this[kParser] + clearIdleSocketValidation(this) + if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { this[kError] = parser.finish() || this[kError] @@ -1007,6 +1025,28 @@ function onSocketClose () { this[kClosed] = true } +function clearIdleSocketValidation (socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]) + socket[kIdleSocketValidationTimeout] = null + } + + socket[kIdleSocketValidation] = 0 +} + +function scheduleIdleSocketValidation (client, socket) { + socket[kIdleSocketValidation] = 1 + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null + socket[kIdleSocketValidation] = 2 + + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume]() + } + }, 0) + socket[kIdleSocketValidationTimeout].unref?.() +} + /** * @param {import('./client.js')} client */ @@ -1024,6 +1064,32 @@ function resumeH1 (client) { socket[kNoRef] = false } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket) + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + return + } + } + + if (client[kRunning] === 0) { + socket[kParser].readMore() + if (socket.destroyed) { + return + } + } + if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) @@ -1122,6 +1188,7 @@ function writeH1 (client, request) { } const socket = client[kSocket] + clearIdleSocketValidation(socket) /** * @param {Error} [err] diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js index e378010513a139..9f27854385c2b5 100644 --- a/deps/undici/src/lib/dispatcher/client-h2.js +++ b/deps/undici/src/lib/dispatcher/client-h2.js @@ -35,6 +35,7 @@ const { kSize, kHTTPContext, kClosed, + kKeepAliveDefaultTimeout, kHeadersTimeout, kBodyTimeout, kEnableConnectProtocol, @@ -152,6 +153,21 @@ function requeueUnsentRequest (client, request) { client[kQueue].splice(client[kPendingIdx] + 1, 0, request) } +function completeRequest (client, request, resetPendingIdx = false) { + const index = client[kQueue].indexOf(request, client[kRunningIdx]) + + if (index === -1 || index >= client[kPendingIdx]) { + return + } + + client[kQueue].splice(index, 1) + client[kPendingIdx]-- + + if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) { + client[kPendingIdx] = client[kRunningIdx] + } +} + function canRetryRequestAfterGoAway (request) { const { body } = request @@ -191,6 +207,7 @@ function connectH2 (client, socket) { session[kClient] = client session[kSocket] = socket session[kHTTP2SessionState] = { + idleTimeout: null, ping: { interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() } @@ -279,10 +296,10 @@ function connectH2 (client, socket) { if (client[kRunning] > 0) { // We are already processing requests - // Non-idempotent request cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - if (request.idempotent === false) return true + // Unlike HTTP/1.1 pipelining, HTTP/2 multiplexes requests on + // independent streams, so non-idempotent requests can be dispatched + // concurrently. Retry eligibility is handled by stream/session error + // handling instead of by serializing all non-idempotent requests. // Don't dispatch an upgrade until all preceding requests have completed. // Possibly, we do not have remote settings confirmed yet. if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true @@ -308,16 +325,66 @@ function connectH2 (client, socket) { function resumeH2 (client) { const socket = client[kSocket] + const session = client[kHTTP2Session] if (socket?.destroyed === false) { if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) { socket.unref() - client[kHTTP2Session].unref() + session.unref() } else { socket.ref() - client[kHTTP2Session].ref() + session.ref() } + + if (client[kSize] === 0 && session[kOpenStreams] === 0) { + setHttp2IdleTimeout(session) + } else { + clearHttp2IdleTimeout(session) + } + } +} + +function clearHttp2IdleTimeout (session) { + const state = session[kHTTP2SessionState] + + if (state?.idleTimeout != null) { + clearTimeout(state.idleTimeout) + state.idleTimeout = null + } +} + +function setHttp2IdleTimeout (session) { + const client = session[kClient] + + if (client[kHTTP2Session] !== session || session.closed || session.destroyed) { + return + } + + if (session[kOpenStreams] !== 0 || client[kSize] !== 0) { + clearHttp2IdleTimeout(session) + return + } + + const state = session[kHTTP2SessionState] + if (state.idleTimeout == null) { + state.idleTimeout = setTimeout(onHttp2SessionIdleTimeout, client[kKeepAliveDefaultTimeout], session).unref() + } +} + +function onHttp2SessionIdleTimeout (session) { + const client = session[kClient] + const socket = session[kSocket] + const state = session[kHTTP2SessionState] + + state.idleTimeout = null + + if (client[kHTTP2Session] !== session || session[kOpenStreams] !== 0 || client[kSize] !== 0 || session.closed || session.destroyed) { + return } + + const err = new InformationalError('socket idle timeout') + socket[kError] = err + util.destroy(socket, err) } function applyConnectionWindowSize (connectionWindowSize) { @@ -445,6 +512,8 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { client[kHTTP2Session] = null } + clearHttp2IdleTimeout(this) + if (!this.closed && !this.destroyed) { this.close() } @@ -467,6 +536,8 @@ function onHttp2SessionClose () { client[kHTTP2Session] = null } + clearHttp2IdleTimeout(this) + if (state.ping.interval != null) { clearInterval(state.ping.interval) state.ping.interval = null @@ -479,7 +550,9 @@ function onHttp2SessionClose () { const requests = client[kQueue].splice(client[kRunningIdx]) for (let i = 0; i < requests.length; i++) { const request = requests[i] - util.errorRequest(client, request, err) + if (request != null) { + util.errorRequest(client, request, err) + } } } } @@ -542,6 +615,7 @@ function closeStreamSession (stream) { session[kOpenStreams] -= 1 if (session[kOpenStreams] === 0) { session.unref() + setHttp2IdleTimeout(session) } } @@ -556,6 +630,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) @@ -698,6 +785,7 @@ function setupUpgradeStream (stream, state) { stream.on('timeout', onUpgradeStreamTimeout) stream.once('close', onUpgradeStreamClose) + clearHttp2IdleTimeout(session) ++session[kOpenStreams] stream.setTimeout(headersTimeout) } @@ -729,11 +817,7 @@ function writeH2 (client, request) { } requestFinalized = true - client[kQueue][client[kRunningIdx]++] = null - - if (resetPendingIdx) { - client[kPendingIdx] = client[kRunningIdx] - } + completeRequest(client, request, resetPendingIdx) client[kResume]() } @@ -970,6 +1054,7 @@ function writeH2 (client, request) { state.stream = stream // Increment counter as we have new streams open + clearHttp2IdleTimeout(session) ++session[kOpenStreams] stream.setTimeout(headersTimeout) @@ -1081,14 +1166,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 +1186,6 @@ function onError (err) { const state = stream[kRequestStreamState] stream.off('error', onError) - - releaseRequestStream(stream) state.abort(err) } @@ -1111,8 +1194,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 +1205,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 +1220,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..8a4f65171bd9eb 100644 --- a/deps/undici/src/lib/dispatcher/client.js +++ b/deps/undici/src/lib/dispatcher/client.js @@ -395,7 +395,9 @@ class Client extends DispatcherBase { const requests = this[kQueue].splice(this[kPendingIdx]) for (let i = 0; i < requests.length; i++) { const request = requests[i] - util.errorRequest(this, request, err) + if (request != null) { + util.errorRequest(this, request, err) + } } const callback = () => { @@ -434,7 +436,9 @@ function onError (client, err) { for (let i = 0; i < requests.length; i++) { const request = requests[i] - util.errorRequest(client, request, err) + if (request != null) { + util.errorRequest(client, request, err) + } } assert(client[kSize] === 0) } @@ -568,9 +572,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/dispatcher/dispatcher-base.js b/deps/undici/src/lib/dispatcher/dispatcher-base.js index aa5928b7c36247..21b5e346acad08 100644 --- a/deps/undici/src/lib/dispatcher/dispatcher-base.js +++ b/deps/undici/src/lib/dispatcher/dispatcher-base.js @@ -38,6 +38,7 @@ class DispatcherBase extends Dispatcher { */ get webSocketOptions () { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default } } diff --git a/deps/undici/src/lib/dispatcher/proxy-agent.js b/deps/undici/src/lib/dispatcher/proxy-agent.js index 8522bdd586d1b7..684565f2d7cf52 100644 --- a/deps/undici/src/lib/dispatcher/proxy-agent.js +++ b/deps/undici/src/lib/dispatcher/proxy-agent.js @@ -147,7 +147,8 @@ class ProxyAgent extends DispatcherBase { factory: agentFactory, username: opts.username || username, password: opts.password || password, - proxyTls: opts.proxyTls + proxyTls: opts.proxyTls, + requestTls: opts.requestTls }) } diff --git a/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js b/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js index 3cc5b19f0be214..bb46b7cfa184be 100644 --- a/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js +++ b/deps/undici/src/lib/dispatcher/socks5-proxy-agent.js @@ -19,6 +19,7 @@ const kProxyAuth = Symbol('proxy auth') const kProxyProtocol = Symbol('proxy protocol') const kPools = Symbol('pools') const kConnector = Symbol('connector') +const kRequestTls = Symbol('request tls settings') // Static flag to ensure warning is only emitted once per process let experimentalWarningEmitted = false @@ -53,6 +54,7 @@ class Socks5ProxyAgent extends DispatcherBase { this[kProxyUrl] = url this[kProxyHeaders] = options.headers || {} this[kProxyProtocol] = options.proxyTls ? 'https:' : 'http:' + this[kRequestTls] = options.requestTls // Extract auth from URL or options this[kProxyAuth] = { @@ -205,9 +207,9 @@ class Socks5ProxyAgent extends DispatcherBase { } debug('upgrading to TLS') finalSocket = tls.connect({ + ...this[kRequestTls], socket, - servername: targetHost, - ...connectOpts.tls || {} + servername: this[kRequestTls]?.servername || targetHost }) const tlsReady = Promise.withResolvers() 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..3a6b75f489f414 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.5.0 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/lib/util/cache.js b/deps/undici/src/lib/util/cache.js index e5abe0f0de0162..a8f112168e60fb 100644 --- a/deps/undici/src/lib/util/cache.js +++ b/deps/undici/src/lib/util/cache.js @@ -228,6 +228,10 @@ function parseCacheControlHeader (header) { headers[headers.length - 1] = lastHeader } + for (let j = 0; j < headers.length; j++) { + headers[j] = headers[j].trim() + } + if (key in output) { output[key] = output[key].concat(headers) } else { @@ -236,10 +240,12 @@ function parseCacheControlHeader (header) { } } else { // Something like `no-cache="some-header"` + const fieldName = value.trim() + if (key in output) { - output[key] = output[key].concat(value) + output[key] = output[key].concat(fieldName) } else { - output[key] = [value] + output[key] = [fieldName] } } diff --git a/deps/undici/src/lib/web/cookies/parse.js b/deps/undici/src/lib/web/cookies/parse.js index 74edf7ec6ac7fc..0e524908077c6d 100644 --- a/deps/undici/src/lib/web/cookies/parse.js +++ b/deps/undici/src/lib/web/cookies/parse.js @@ -4,7 +4,6 @@ const { collectASequenceOfCodePointsFast } = require('../infra') const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') const { isCTLExcludingHtab } = require('./util') const assert = require('node:assert') -const { unescape: qsUnescape } = require('node:querystring') /** * @description Parses the field-value attributes of a set-cookie header string. @@ -82,7 +81,7 @@ function parseSetCookie (header) { // store arbitrary data in a cookie-value SHOULD encode that data, for // example, using Base64 [RFC4648]. return { - name, value: qsUnescape(value), ...parseUnparsedAttributes(unparsedAttributes) + name, value, ...parseUnparsedAttributes(unparsedAttributes) } } @@ -280,32 +279,25 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // If the attribute-name case-insensitively matches the string // "SameSite", the user agent MUST process the cookie-av as follows: - // 1. Let enforcement be "Default". - let enforcement = 'Default' - const attributeValueLowercase = attributeValue.toLowerCase() - // 2. If cookie-av's attribute-value is a case-insensitive match for - // "None", set enforcement to "None". - if (attributeValueLowercase.includes('none')) { - enforcement = 'None' - } - // 3. If cookie-av's attribute-value is a case-insensitive match for - // "Strict", set enforcement to "Strict". - if (attributeValueLowercase.includes('strict')) { - enforcement = 'Strict' + // 1. If cookie-av's attribute-value is a case-insensitive match for + // "None", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "None". + if (attributeValueLowercase === 'none') { + cookieAttributeList.sameSite = 'None' + } else if (attributeValueLowercase === 'strict') { + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", append an attribute to the cookie-attribute-list with + // an attribute-name of "SameSite" and an attribute-value of + // "Strict". + cookieAttributeList.sameSite = 'Strict' + } else if (attributeValueLowercase === 'lax') { + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of "Lax". + cookieAttributeList.sameSite = 'Lax' } - - // 4. If cookie-av's attribute-value is a case-insensitive match for - // "Lax", set enforcement to "Lax". - if (attributeValueLowercase.includes('lax')) { - enforcement = 'Lax' - } - - // 5. Append an attribute to the cookie-attribute-list with an - // attribute-name of "SameSite" and an attribute-value of - // enforcement. - cookieAttributeList.sameSite = enforcement } else { cookieAttributeList.unparsed ??= [] diff --git a/deps/undici/src/lib/web/eventsource/eventsource.js b/deps/undici/src/lib/web/eventsource/eventsource.js index 32dcf0e423e06f..17a1de7b7ba6bc 100644 --- a/deps/undici/src/lib/web/eventsource/eventsource.js +++ b/deps/undici/src/lib/web/eventsource/eventsource.js @@ -2,7 +2,6 @@ const { pipeline } = require('node:stream') const { fetching } = require('../fetch') -const { makeRequest } = require('../fetch/request') const { webidl } = require('../webidl') const { EventSourceStream } = require('./eventsource-stream') const { parseMIMEType } = require('../fetch/data-url') @@ -10,6 +9,7 @@ const { createFastMessageEvent } = require('../websocket/events') const { isNetworkError } = require('../fetch/response') const { kEnumerableProperty } = require('../../core/util') const { environmentSettingsObject } = require('../fetch/util') +const { createPotentialCORSRequest } = require('./util') let experimentalWarned = false @@ -160,33 +160,22 @@ class EventSource extends EventTarget { // 8. Let request be the result of creating a potential-CORS request given // urlRecord, the empty string, and corsAttributeState. - const initRequest = { - redirect: 'follow', - keepalive: true, - // @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes - mode: 'cors', - credentials: corsAttributeState === 'anonymous' - ? 'same-origin' - : 'omit', - referrer: 'no-referrer' - } + const request = createPotentialCORSRequest(urlRecord, '', corsAttributeState) // 9. Set request's client to settings. - initRequest.client = environmentSettingsObject.settingsObject + request.client = environmentSettingsObject.settingsObject // 10. User agents may set (`Accept`, `text/event-stream`) in request's header list. - initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]] + request.headersList.set('Accept', 'text/event-stream') // 11. Set request's cache mode to "no-store". - initRequest.cache = 'no-store' + request.cache = 'no-store' // 12. Set request's initiator type to "other". - initRequest.initiator = 'other' - - initRequest.urlList = [new URL(this.#url)] + request.initiator = 'other' // 13. Set ev's request to request. - this.#request = makeRequest(initRequest) + this.#request = request this.#connect() } diff --git a/deps/undici/src/lib/web/eventsource/util.js b/deps/undici/src/lib/web/eventsource/util.js index a87cc834ecab40..4f106bb40618bd 100644 --- a/deps/undici/src/lib/web/eventsource/util.js +++ b/deps/undici/src/lib/web/eventsource/util.js @@ -1,5 +1,7 @@ 'use strict' +const { makeRequest } = require('../fetch/request') + /** * Checks if the given value is a valid LastEventId. * @param {string} value @@ -23,7 +25,36 @@ function isASCIINumber (value) { return true } +function createPotentialCORSRequest (url, destination, corsAttributeState, sameOriginFallback) { + // 1. Let mode be "no-cors" if corsAttributeState is No CORS, and "cors" otherwise. + let mode = corsAttributeState === 'no cors' ? 'no-cors' : 'cors' + + // 2. If same-origin fallback flag is set and mode is "no-cors", set mode to "same-origin". + if (sameOriginFallback && mode === 'no-cors') { + mode = 'same-origin' + } + + // 3. Let credentialsMode be "include". + let credentialsMode = 'include' + + // 4. If corsAttributeState is Anonymous, set credentialsMode to "same-origin". + if (corsAttributeState === 'anonymous') { + credentialsMode = 'same-origin' + } + + // 5. Return a new request whose URL is url, destination is destination, mode is mode, + // credentials mode is credentialsMode, and whose use-URL-credentials flag is set. + return makeRequest({ + urlList: [url], + destination, + mode, + credentials: credentialsMode, + useCredentials: true + }) +} + module.exports = { isValidLastEventId, - isASCIINumber + isASCIINumber, + createPotentialCORSRequest } diff --git a/deps/undici/src/lib/web/fetch/body.js b/deps/undici/src/lib/web/fetch/body.js index 525bafb2c3c56e..22ae143882dc00 100644 --- a/deps/undici/src/lib/web/fetch/body.js +++ b/deps/undici/src/lib/web/fetch/body.js @@ -392,6 +392,49 @@ function bodyMixinMethods (instance, getInternalState) { return consumeBody(this, (bytes) => { return new Uint8Array(bytes) }, instance, getInternalState) + }, + + textStream () { + const this_ = getInternalState(this) + + // 1. If this is unusable, then throw a TypeError. + if (bodyUnusable(this_)) { + throw new TypeError('Body is unusable: Body has already been read') + } + + // 2. If this’s body is null: + if (this_.body == null) { + // 2.1. Let emptyStream be a new ReadableStream in this’s relevant realm. + // 2.2. Set up emptyStream. + /** @type {ReadableStreamDefaultController} */ + let controller + const emptyStream = new ReadableStream({ + start: (c) => { + controller = c + }, + pull: () => Promise.resolve(), + cancel: () => Promise.resolve() + }, { + size: () => 1 + }) + + // 2.3. Close emptyStream. + controller.close() + + // 2.4. Return emptyStream. + return emptyStream + } + + // 3. Let stream be this’s body’s stream. + /** @type {ReadableStream} */ + const stream = this_.body.stream + + // 4. Let decoder be a new TextDecoderStream object in this’s relevant realm. + // 5. Set up decoder with UTF-8. + const decoder = new TextDecoderStream('UTF-8') + + // 6. Return the result of stream, piped through decoder. + return stream.pipeThrough(decoder) } } diff --git a/deps/undici/src/lib/web/fetch/request.js b/deps/undici/src/lib/web/fetch/request.js index 1fb6b8a45e5b7e..dbe809c289c734 100644 --- a/deps/undici/src/lib/web/fetch/request.js +++ b/deps/undici/src/lib/web/fetch/request.js @@ -930,6 +930,7 @@ function makeRequest (init) { referrerPolicy: init.referrerPolicy ?? '', mode: init.mode ?? 'no-cors', useCORSPreflightFlag: init.useCORSPreflightFlag ?? false, + // TODO: is this credentials mode? https://fetch.spec.whatwg.org/#concept-request-credentials-mode credentials: init.credentials ?? 'same-origin', useCredentials: init.useCredentials ?? false, cache: init.cache ?? 'default', diff --git a/deps/undici/src/lib/web/websocket/receiver.js b/deps/undici/src/lib/web/websocket/receiver.js index 9221e5cc54fccf..cfb5a54cbc6413 100644 --- a/deps/undici/src/lib/web/websocket/receiver.js +++ b/deps/undici/src/lib/web/websocket/receiver.js @@ -39,6 +39,9 @@ class ByteParser extends Writable { /** @type {import('./websocket').Handler} */ #handler + /** @type {number} */ + #maxFragments + /** @type {number} */ #maxPayloadSize @@ -52,6 +55,7 @@ class ByteParser extends Writable { this.#handler = handler this.#extensions = extensions == null ? new Map() : extensions + this.#maxFragments = options.maxFragments ?? 0 this.#maxPayloadSize = options.maxPayloadSize ?? 0 if (this.#extensions.has('permessage-deflate')) { @@ -75,7 +79,7 @@ class ByteParser extends Writable { if ( this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && - this.#info.payloadLength > this.#maxPayloadSize + this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize ) { failWebsocketConnection(this.#handler, 1009, 'Payload size exceeds maximum allowed size') return false @@ -242,7 +246,9 @@ class ByteParser extends Writable { this.#state = parserStates.INFO } else { if (!this.#info.compressed) { - this.writeFragments(body) + if (!this.writeFragments(body)) { + return + } // If the frame is not fragmented, a message has been received. // If the frame is fragmented, it will terminate with a fin bit set @@ -264,7 +270,9 @@ class ByteParser extends Writable { return } - this.writeFragments(data) + if (!this.writeFragments(data)) { + return + } // Check cumulative fragment size if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { @@ -345,8 +353,17 @@ class ByteParser extends Writable { } writeFragments (fragment) { + if ( + this.#maxFragments > 0 && + this.#fragments.length === this.#maxFragments + ) { + failWebsocketConnection(this.#handler, 1008, 'Too many message fragments') + return false + } + this.#fragmentsBytes += fragment.length this.#fragments.push(fragment) + return true } consumeFragments () { diff --git a/deps/undici/src/lib/web/websocket/stream/websocketstream.js b/deps/undici/src/lib/web/websocket/stream/websocketstream.js index 1a070e2f979fe7..383fd0bf7aabf1 100644 --- a/deps/undici/src/lib/web/websocket/stream/websocketstream.js +++ b/deps/undici/src/lib/web/websocket/stream/websocketstream.js @@ -258,7 +258,14 @@ class WebSocketStream { #onConnectionEstablished (response, parsedExtensions) { this.#handler.socket = response.socket - const parser = new ByteParser(this.#handler, parsedExtensions) + // Get options from dispatcher options + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments + const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize + + const parser = new ByteParser(this.#handler, parsedExtensions, { + maxFragments, + maxPayloadSize + }) parser.on('drain', () => this.#handler.onParserDrain()) parser.on('error', (err) => this.#handler.onParserError(err)) diff --git a/deps/undici/src/lib/web/websocket/websocket.js b/deps/undici/src/lib/web/websocket/websocket.js index a2abd9c9ab60d9..e473a1bc4917e4 100644 --- a/deps/undici/src/lib/web/websocket/websocket.js +++ b/deps/undici/src/lib/web/websocket/websocket.js @@ -468,10 +468,12 @@ class WebSocket extends EventTarget { // once this happens, the connection is open this.#handler.socket = response.socket - // Get maxPayloadSize from dispatcher options + // Get options from dispatcher options + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize const parser = new ByteParser(this.#handler, parsedExtensions, { + maxFragments, maxPayloadSize }) parser.on('drain', () => this.#handler.onParserDrain()) diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json index d574145badb9f2..91a9c4dc24d246 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.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undici", - "version": "8.4.0", + "version": "8.5.0", "license": "MIT", "devDependencies": { "@fastify/busboy": "3.2.0", diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index 6fb07c18274458..b22e7b051b1124 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.5.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { diff --git a/deps/undici/src/repro-h2-pipelining-default.mjs b/deps/undici/src/repro-h2-pipelining-default.mjs deleted file mode 100644 index 82616363fed822..00000000000000 --- a/deps/undici/src/repro-h2-pipelining-default.mjs +++ /dev/null @@ -1,78 +0,0 @@ -// Repro for the H2 default-pipelining bottleneck described in #4143. -// -// Since 8.0.0 allowH2 defaults to true, but pipelining still defaults to 1. -// On a single shared H2 session (connections=1) that serializes concurrent -// fetch() calls into one in-flight stream at a time, instead of multiplexing. -// Without connections set, Agent works around it by opening one TCP socket -// per concurrent request — defeating H2 multiplexing entirely and creating -// extra TLS handshakes. -// -// Run: -// node repro-h2-pipelining-default.mjs -// -// Expected output on main: -// default (allowH2=true, p=1) total~=1s sockets=5 h2sessions=5 (one socket per req) -// connections=1, p=1 (default) total~=5s sockets=1 h2sessions=1 (serialized!) -// connections=1, pipelining=100 total~=1s sockets=1 h2sessions=1 (multiplexed) - -import { createSecureServer } from 'node:http2' -import { once } from 'node:events' -import pem from '@metcoder95/https-pem' -import { fetch, Agent } from './index.js' - -const N = 5 -const DELAY = 1000 - -const server = createSecureServer({ - ...(await pem.generate({ opts: { keySize: 2048 } })), - allowHTTP1: true -}) -let inFlight = 0 -let peakInFlight = 0 -const arrivedAt = [] -const sockets = new Set() -const sessions = new Set() -server.on('session', (s) => sessions.add(s)) -server.on('connection', (sock) => sockets.add(sock)) -server.on('stream', (stream) => { - arrivedAt.push(Date.now()) - inFlight++ - peakInFlight = Math.max(peakInFlight, inFlight) - setTimeout(() => { - inFlight-- - stream.respond({ ':status': 200 }) - stream.end('ok') - }, DELAY) -}) -server.listen(0) -await once(server, 'listening') -const url = `https://localhost:${server.address().port}/` - -async function run (label, dispatcher) { - arrivedAt.length = 0 - peakInFlight = 0 - sockets.clear() - sessions.clear() - const t0 = Date.now() - await Promise.all( - Array.from({ length: N }, () => - fetch(url, { dispatcher }).then(r => r.text()) - ) - ) - const total = Date.now() - t0 - const spreadMs = arrivedAt.at(-1) - arrivedAt[0] - console.log( - `${label.padEnd(28)} total=${total}ms ` + - `peak=${peakInFlight} ` + - `sockets=${sockets.size} h2sessions=${sessions.size} ` + - `spread=${spreadMs}ms` - ) - await dispatcher.close() -} - -const tlsOpts = { connect: { rejectUnauthorized: false } } -await run('default (allowH2=true, p=1)', new Agent(tlsOpts)) -await run('connections=1, p=1 (default)', new Agent({ ...tlsOpts, connections: 1 })) -await run('connections=1, pipelining=100', new Agent({ ...tlsOpts, connections: 1, pipelining: 100 })) - -server.close() diff --git a/deps/undici/src/types/client.d.ts b/deps/undici/src/types/client.d.ts index 530a94583898a9..b869eafa34ef09 100644 --- a/deps/undici/src/types/client.d.ts +++ b/deps/undici/src/types/client.d.ts @@ -116,6 +116,11 @@ export declare namespace Client { bytesRead?: number } export interface WebSocketOptions { + /** + * Maximum number of fragments in a message. Set to 0 to disable the limit. + * @default 131072 + */ + maxFragments?: number; /** * Maximum allowed payload size in bytes for WebSocket messages. * Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. diff --git a/deps/undici/src/types/fetch.d.ts b/deps/undici/src/types/fetch.d.ts index d4fc1d2219658c..df9218ada5073b 100644 --- a/deps/undici/src/types/fetch.d.ts +++ b/deps/undici/src/types/fetch.d.ts @@ -57,6 +57,7 @@ export class BodyMixin { readonly formData: () => Promise readonly json: () => Promise readonly text: () => Promise + readonly textStream: () => ReadableStream } export interface SpecIterator { diff --git a/deps/undici/undici.js b/deps/undici/undici.js index c62257ae159057..b59cea64f3c361 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -651,6 +651,7 @@ var require_dispatcher_base = __commonJS({ */ get webSocketOptions() { return { + maxFragments: this[kWebSocketOptions].maxFragments ?? 131072, maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default }; @@ -6899,6 +6900,29 @@ Content-Type: ${value.type || "application/octet-stream"}\r return consumeBody(this, (bytes) => { return new Uint8Array(bytes); }, instance, getInternalState); + }, + textStream() { + const this_ = getInternalState(this); + if (bodyUnusable(this_)) { + throw new TypeError("Body is unusable: Body has already been read"); + } + if (this_.body == null) { + let controller; + const emptyStream = new ReadableStream({ + start: /* @__PURE__ */ __name((c) => { + controller = c; + }, "start"), + pull: /* @__PURE__ */ __name(() => Promise.resolve(), "pull"), + cancel: /* @__PURE__ */ __name(() => Promise.resolve(), "cancel") + }, { + size: /* @__PURE__ */ __name(() => 1, "size") + }); + controller.close(); + return emptyStream; + } + const stream = this_.body.stream; + const decoder = new TextDecoderStream("UTF-8"); + return stream.pipeThrough(decoder); } }; return methods; @@ -7018,6 +7042,9 @@ var require_client_h1 = __commonJS({ var EMPTY_BUF = Buffer.alloc(0); var FastBuffer = Buffer[Symbol.species]; var removeAllListeners = util.removeAllListeners; + var kIdleSocketValidation = /* @__PURE__ */ Symbol("kIdleSocketValidation"); + var kIdleSocketValidationTimeout = /* @__PURE__ */ Symbol("kIdleSocketValidationTimeout"); + var kSocketUsed = /* @__PURE__ */ Symbol("kSocketUsed"); var extractBody; function lazyllhttp() { const llhttpWasmData = process.env.JEST_WORKER_ID ? require_llhttp_wasm() : void 0; @@ -7262,7 +7289,6 @@ var require_client_h1 = __commonJS({ finish() { assert(currentParser === null); assert(this.ptr != null); - assert(!this.paused); const { llhttp } = this; let ret; try { @@ -7320,6 +7346,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError("bad response", util.getSocketInfo(socket))); + return -1; + } const request = client[kQueue][client[kRunningIdx]]; if (!request) { return -1; @@ -7427,6 +7457,10 @@ var require_client_h1 = __commonJS({ if (socket.destroyed) { return -1; } + if (client[kRunning] === 0) { + util.destroy(socket, new SocketError("bad response", util.getSocketInfo(socket))); + return -1; + } const request = client[kQueue][client[kRunningIdx]]; if (!request) { return -1; @@ -7560,6 +7594,7 @@ var require_client_h1 = __commonJS({ } request.onResponseEnd(headers); client[kQueue][client[kRunningIdx]++] = null; + socket[kSocketUsed] = client[kPending] === 0; if (socket[kWriting]) { assert(client[kRunning] === 0); util.destroy(socket, new InformationalError("reset")); @@ -7614,6 +7649,9 @@ var require_client_h1 = __commonJS({ socket[kWriting] = false; socket[kReset] = false; socket[kBlocking] = false; + socket[kIdleSocketValidation] = 0; + socket[kIdleSocketValidationTimeout] = null; + socket[kSocketUsed] = false; socket[kParser] = new Parser(client, socket, llhttpInstance); util.addListener(socket, "error", onHttpSocketError); util.addListener(socket, "readable", onHttpSocketReadable); @@ -7653,7 +7691,7 @@ var require_client_h1 = __commonJS({ * @returns {boolean} */ busy(request) { - if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking] || socket[kIdleSocketValidation] === 1) { return true; } if (request) { @@ -7705,6 +7743,7 @@ var require_client_h1 = __commonJS({ __name(onHttpSocketEnd, "onHttpSocketEnd"); function onHttpSocketClose() { const parser = this[kParser]; + clearIdleSocketValidation(this); if (parser) { if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { this[kError] = parser.finish() || this[kError]; @@ -7738,6 +7777,26 @@ var require_client_h1 = __commonJS({ this[kClosed] = true; } __name(onSocketClose, "onSocketClose"); + function clearIdleSocketValidation(socket) { + if (socket[kIdleSocketValidationTimeout]) { + clearTimeout(socket[kIdleSocketValidationTimeout]); + socket[kIdleSocketValidationTimeout] = null; + } + socket[kIdleSocketValidation] = 0; + } + __name(clearIdleSocketValidation, "clearIdleSocketValidation"); + function scheduleIdleSocketValidation(client, socket) { + socket[kIdleSocketValidation] = 1; + socket[kIdleSocketValidationTimeout] = setTimeout(() => { + socket[kIdleSocketValidationTimeout] = null; + socket[kIdleSocketValidation] = 2; + if (client[kSocket] === socket && !socket.destroyed) { + client[kResume](); + } + }, 0); + socket[kIdleSocketValidationTimeout].unref?.(); + } + __name(scheduleIdleSocketValidation, "scheduleIdleSocketValidation"); function resumeH1(client) { const socket = client[kSocket]; if (socket && !socket.destroyed) { @@ -7750,6 +7809,29 @@ var require_client_h1 = __commonJS({ socket.ref(); socket[kNoRef] = false; } + if (client[kRunning] === 0 && client[kPending] > 0 && socket[kSocketUsed]) { + if (socket[kIdleSocketValidation] === 0) { + scheduleIdleSocketValidation(client, socket); + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + return; + } + if (socket[kIdleSocketValidation] === 1) { + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + return; + } + } + if (client[kRunning] === 0) { + socket[kParser].readMore(); + if (socket.destroyed) { + return; + } + } if (client[kSize] === 0) { if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE); @@ -7804,6 +7886,7 @@ var require_client_h1 = __commonJS({ process.emitWarning(new RequestContentLengthMismatchError()); } const socket = client[kSocket]; + clearIdleSocketValidation(socket); const abort = /* @__PURE__ */ __name((err) => { if (request.aborted || request.completed) { return; @@ -8231,6 +8314,7 @@ var require_client_h2 = __commonJS({ kSize, kHTTPContext, kClosed, + kKeepAliveDefaultTimeout, kHeadersTimeout, kBodyTimeout, kEnableConnectProtocol, @@ -8331,6 +8415,18 @@ var require_client_h2 = __commonJS({ client[kQueue].splice(client[kPendingIdx] + 1, 0, request); } __name(requeueUnsentRequest, "requeueUnsentRequest"); + function completeRequest(client, request, resetPendingIdx = false) { + const index = client[kQueue].indexOf(request, client[kRunningIdx]); + if (index === -1 || index >= client[kPendingIdx]) { + return; + } + client[kQueue].splice(index, 1); + client[kPendingIdx]--; + if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) { + client[kPendingIdx] = client[kRunningIdx]; + } + } + __name(completeRequest, "completeRequest"); function canRetryRequestAfterGoAway(request) { const { body } = request; return body == null || util.isBuffer(body) || util.isBlobLike(body); @@ -8365,6 +8461,7 @@ var require_client_h2 = __commonJS({ session[kClient] = client; session[kSocket] = socket; session[kHTTP2SessionState] = { + idleTimeout: null, ping: { interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() } @@ -8435,7 +8532,6 @@ var require_client_h2 = __commonJS({ } if (request != null) { if (client[kRunning] > 0) { - if (request.idempotent === false) return true; if ((request.upgrade === "websocket" || request.method === "CONNECT") && session[kRemoteSettings] === false) return true; if (util.bodyLength(request.body) !== 0 && (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true; } else { @@ -8449,17 +8545,59 @@ var require_client_h2 = __commonJS({ __name(connectH2, "connectH2"); function resumeH2(client) { const socket = client[kSocket]; + const session = client[kHTTP2Session]; if (socket?.destroyed === false) { if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) { socket.unref(); - client[kHTTP2Session].unref(); + session.unref(); } else { socket.ref(); - client[kHTTP2Session].ref(); + session.ref(); + } + if (client[kSize] === 0 && session[kOpenStreams] === 0) { + setHttp2IdleTimeout(session); + } else { + clearHttp2IdleTimeout(session); } } } __name(resumeH2, "resumeH2"); + function clearHttp2IdleTimeout(session) { + const state = session[kHTTP2SessionState]; + if (state?.idleTimeout != null) { + clearTimeout(state.idleTimeout); + state.idleTimeout = null; + } + } + __name(clearHttp2IdleTimeout, "clearHttp2IdleTimeout"); + function setHttp2IdleTimeout(session) { + const client = session[kClient]; + if (client[kHTTP2Session] !== session || session.closed || session.destroyed) { + return; + } + if (session[kOpenStreams] !== 0 || client[kSize] !== 0) { + clearHttp2IdleTimeout(session); + return; + } + const state = session[kHTTP2SessionState]; + if (state.idleTimeout == null) { + state.idleTimeout = setTimeout(onHttp2SessionIdleTimeout, client[kKeepAliveDefaultTimeout], session).unref(); + } + } + __name(setHttp2IdleTimeout, "setHttp2IdleTimeout"); + function onHttp2SessionIdleTimeout(session) { + const client = session[kClient]; + const socket = session[kSocket]; + const state = session[kHTTP2SessionState]; + state.idleTimeout = null; + if (client[kHTTP2Session] !== session || session[kOpenStreams] !== 0 || client[kSize] !== 0 || session.closed || session.destroyed) { + return; + } + const err = new InformationalError("socket idle timeout"); + socket[kError] = err; + util.destroy(socket, err); + } + __name(onHttp2SessionIdleTimeout, "onHttp2SessionIdleTimeout"); function applyConnectionWindowSize(connectionWindowSize) { try { if (typeof this.setLocalWindowSize === "function") { @@ -8555,6 +8693,7 @@ var require_client_h2 = __commonJS({ client[kHTTPContext] = null; client[kHTTP2Session] = null; } + clearHttp2IdleTimeout(this); if (!this.closed && !this.destroyed) { this.close(); } @@ -8571,6 +8710,7 @@ var require_client_h2 = __commonJS({ client[kHTTPContext] = null; client[kHTTP2Session] = null; } + clearHttp2IdleTimeout(this); if (state.ping.interval != null) { clearInterval(state.ping.interval); state.ping.interval = null; @@ -8580,7 +8720,9 @@ var require_client_h2 = __commonJS({ const requests = client[kQueue].splice(client[kRunningIdx]); for (let i = 0; i < requests.length; i++) { const request = requests[i]; - util.errorRequest(client, request, err); + if (request != null) { + util.errorRequest(client, request, err); + } } } } @@ -8629,6 +8771,7 @@ var require_client_h2 = __commonJS({ session[kOpenStreams] -= 1; if (session[kOpenStreams] === 0) { session.unref(); + setHttp2IdleTimeout(session); } } __name(closeStreamSession, "closeStreamSession"); @@ -8641,6 +8784,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); @@ -8760,6 +8911,7 @@ var require_client_h2 = __commonJS({ stream.once("end", onUpgradeStreamEnd); stream.on("timeout", onUpgradeStreamTimeout); stream.once("close", onUpgradeStreamClose); + clearHttp2IdleTimeout(session); ++session[kOpenStreams]; stream.setTimeout(headersTimeout); } @@ -8784,10 +8936,7 @@ var require_client_h2 = __commonJS({ return; } requestFinalized = true; - client[kQueue][client[kRunningIdx]++] = null; - if (resetPendingIdx) { - client[kPendingIdx] = client[kRunningIdx]; - } + completeRequest(client, request, resetPendingIdx); client[kResume](); }, "finalizeRequest"); const abort = /* @__PURE__ */ __name((err, resetPendingIdx = false) => { @@ -8940,6 +9089,7 @@ var require_client_h2 = __commonJS({ stream[kHTTP2Stream] = true; stream[kRequestStreamState] = state; state.stream = stream; + clearHttp2IdleTimeout(session); ++session[kOpenStreams]; stream.setTimeout(headersTimeout); stream[kHTTP2Session] = session; @@ -9028,12 +9178,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 +9191,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 +9198,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 +9208,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 +9218,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() { @@ -9584,7 +9729,9 @@ var require_client = __commonJS({ const requests = this[kQueue].splice(this[kPendingIdx]); for (let i = 0; i < requests.length; i++) { const request = requests[i]; - util.errorRequest(this, request, err); + if (request != null) { + util.errorRequest(this, request, err); + } } const callback = /* @__PURE__ */ __name(() => { if (this[kClosedResolve]) { @@ -9609,7 +9756,9 @@ var require_client = __commonJS({ const requests = client[kQueue].splice(client[kRunningIdx]); for (let i = 0; i < requests.length; i++) { const request = requests[i]; - util.errorRequest(client, request, err); + if (request != null) { + util.errorRequest(client, request, err); + } } assert(client[kSize] === 0); } @@ -9719,9 +9868,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 { @@ -10760,6 +10913,7 @@ var require_socks5_proxy_agent = __commonJS({ var kProxyProtocol = /* @__PURE__ */ Symbol("proxy protocol"); var kPools = /* @__PURE__ */ Symbol("pools"); var kConnector = /* @__PURE__ */ Symbol("connector"); + var kRequestTls = /* @__PURE__ */ Symbol("request tls settings"); var experimentalWarningEmitted = false; var Socks5ProxyAgent = class extends DispatcherBase { static { @@ -10784,6 +10938,7 @@ var require_socks5_proxy_agent = __commonJS({ this[kProxyUrl] = url; this[kProxyHeaders] = options.headers || {}; this[kProxyProtocol] = options.proxyTls ? "https:" : "http:"; + this[kRequestTls] = options.requestTls; this[kProxyAuth] = { username: options.username || (url.username ? decodeURIComponent(url.username) : null), password: options.password || (url.password ? decodeURIComponent(url.password) : null) @@ -10891,9 +11046,9 @@ var require_socks5_proxy_agent = __commonJS({ } debug("upgrading to TLS"); finalSocket = tls.connect({ + ...this[kRequestTls], socket, - servername: targetHost, - ...connectOpts.tls || {} + servername: this[kRequestTls]?.servername || targetHost }); const tlsReady = Promise.withResolvers(); finalSocket.once("secureConnect", tlsReady.resolve); @@ -11076,7 +11231,8 @@ var require_proxy_agent = __commonJS({ factory: agentFactory, username: opts.username || username, password: opts.password || password, - proxyTls: opts.proxyTls + proxyTls: opts.proxyTls, + requestTls: opts.requestTls }); } if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") { @@ -12867,6 +13023,7 @@ var require_request2 = __commonJS({ referrerPolicy: init.referrerPolicy ?? "", mode: init.mode ?? "no-cors", useCORSPreflightFlag: init.useCORSPreflightFlag ?? false, + // TODO: is this credentials mode? https://fetch.spec.whatwg.org/#concept-request-credentials-mode credentials: init.credentials ?? "same-origin", useCredentials: init.useCredentials ?? false, cache: init.cache ?? "default", @@ -15262,6 +15419,8 @@ var require_receiver = __commonJS({ /** @type {import('./websocket').Handler} */ #handler; /** @type {number} */ + #maxFragments; + /** @type {number} */ #maxPayloadSize; /** * @param {import('./websocket').Handler} handler @@ -15272,6 +15431,7 @@ var require_receiver = __commonJS({ super(); this.#handler = handler; this.#extensions = extensions == null ? /* @__PURE__ */ new Map() : extensions; + this.#maxFragments = options.maxFragments ?? 0; this.#maxPayloadSize = options.maxPayloadSize ?? 0; if (this.#extensions.has("permessage-deflate")) { this.#extensions.set("permessage-deflate", new PerMessageDeflate(extensions, options)); @@ -15288,7 +15448,7 @@ var require_receiver = __commonJS({ this.run(callback); } #validatePayloadLength() { - if (this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && this.#info.payloadLength > this.#maxPayloadSize) { + if (this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && this.#info.payloadLength + this.#fragmentsBytes > this.#maxPayloadSize) { failWebsocketConnection(this.#handler, 1009, "Payload size exceeds maximum allowed size"); return false; } @@ -15405,7 +15565,9 @@ var require_receiver = __commonJS({ this.#state = parserStates.INFO; } else { if (!this.#info.compressed) { - this.writeFragments(body); + if (!this.writeFragments(body)) { + return; + } if (!this.#info.fragmented && this.#info.fin) { websocketMessageReceived(this.#handler, this.#info.binaryType, this.consumeFragments()); } @@ -15420,7 +15582,9 @@ var require_receiver = __commonJS({ failWebsocketConnection(this.#handler, code, error.message); return; } - this.writeFragments(data); + if (!this.writeFragments(data)) { + return; + } if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { failWebsocketConnection(this.#handler, 1009, new MessageSizeExceededError().message); return; @@ -15485,8 +15649,13 @@ var require_receiver = __commonJS({ } } writeFragments(fragment) { + if (this.#maxFragments > 0 && this.#fragments.length === this.#maxFragments) { + failWebsocketConnection(this.#handler, 1008, "Too many message fragments"); + return false; + } this.#fragmentsBytes += fragment.length; this.#fragments.push(fragment); + return true; } consumeFragments() { const fragments = this.#fragments; @@ -15962,8 +16131,10 @@ var require_websocket = __commonJS({ */ #onConnectionEstablished(response, parsedExtensions) { this.#handler.socket = response.socket; + const maxFragments = this.#handler.controller.dispatcher?.webSocketOptions?.maxFragments; const maxPayloadSize = this.#handler.controller.dispatcher?.webSocketOptions?.maxPayloadSize; const parser = new ByteParser(this.#handler, parsedExtensions, { + maxFragments, maxPayloadSize }); parser.on("drain", () => this.#handler.onParserDrain()); @@ -16163,6 +16334,7 @@ var require_websocket = __commonJS({ var require_util4 = __commonJS({ "lib/web/eventsource/util.js"(exports2, module2) { "use strict"; + var { makeRequest } = require_request2(); function isValidLastEventId(value) { return value.indexOf("\0") === -1; } @@ -16175,9 +16347,28 @@ var require_util4 = __commonJS({ return true; } __name(isASCIINumber, "isASCIINumber"); + function createPotentialCORSRequest(url, destination, corsAttributeState, sameOriginFallback) { + let mode = corsAttributeState === "no cors" ? "no-cors" : "cors"; + if (sameOriginFallback && mode === "no-cors") { + mode = "same-origin"; + } + let credentialsMode = "include"; + if (corsAttributeState === "anonymous") { + credentialsMode = "same-origin"; + } + return makeRequest({ + urlList: [url], + destination, + mode, + credentials: credentialsMode, + useCredentials: true + }); + } + __name(createPotentialCORSRequest, "createPotentialCORSRequest"); module2.exports = { isValidLastEventId, - isASCIINumber + isASCIINumber, + createPotentialCORSRequest }; } }); @@ -16535,7 +16726,6 @@ var require_eventsource = __commonJS({ "use strict"; var { pipeline } = require("node:stream"); var { fetching } = require_fetch(); - var { makeRequest } = require_request2(); var { webidl } = require_webidl(); var { EventSourceStream } = require_eventsource_stream(); var { parseMIMEType } = require_data_url(); @@ -16543,6 +16733,7 @@ var require_eventsource = __commonJS({ var { isNetworkError } = require_response(); var { kEnumerableProperty } = require_util(); var { environmentSettingsObject } = require_util2(); + var { createPotentialCORSRequest } = require_util4(); var experimentalWarned = false; var defaultReconnectionTime = 3e3; var CONNECTING = 0; @@ -16610,20 +16801,12 @@ var require_eventsource = __commonJS({ corsAttributeState = USE_CREDENTIALS; this.#withCredentials = true; } - const initRequest = { - redirect: "follow", - keepalive: true, - // @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes - mode: "cors", - credentials: corsAttributeState === "anonymous" ? "same-origin" : "omit", - referrer: "no-referrer" - }; - initRequest.client = environmentSettingsObject.settingsObject; - initRequest.headersList = [["accept", { name: "accept", value: "text/event-stream" }]]; - initRequest.cache = "no-store"; - initRequest.initiator = "other"; - initRequest.urlList = [new URL(this.#url)]; - this.#request = makeRequest(initRequest); + const request = createPotentialCORSRequest(urlRecord, "", corsAttributeState); + request.client = environmentSettingsObject.settingsObject; + request.headersList.set("Accept", "text/event-stream"); + request.cache = "no-store"; + request.initiator = "other"; + this.#request = request; this.#connect(); } /** diff --git a/src/undici_version.h b/src/undici_version.h index 438598cb5da9ac..30557776915f7a 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.5.0" #endif // SRC_UNDICI_VERSION_H_