Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions deps/undici/src/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ AGENTS.md
.githuman

benchmarks/package-lock.json

# File generated by /scripts/platform-shell.js
scripts/.npmrc
5 changes: 5 additions & 0 deletions deps/undici/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@ For more information about their behavior, please reference the body mixin from

This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.

For the top-level APIs below, the `url` argument supplies the request origin and
path. Do not pass `origin` or `path` in the second `options` argument. The linked
`Dispatcher` option types include those fields because dispatcher methods are
lower-level APIs that do not receive a separate `url` argument.

### `undici.request([url, options]): Promise`

Arguments:
Expand Down
21 changes: 21 additions & 0 deletions deps/undici/src/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ meet the following criteria:
* Dependencies installed by the application.
* The DNS resolution results provided by the operating system or configured
resolvers.
* The network environment in which the process runs, including any private or
internal networks it can reach, for the privacy of traffic on that network.
Deployments that require stronger isolation, monitoring, or egress controls
are responsible for providing them.
* Any proxy server configured by the application, runtime, or environment.
Proxies are trusted network intermediaries and must be authorized by the
relevant network or deployment authority. Undici's proxy support is for
routing traffic through such proxies, for example to reach external networks
through a firewall; it is not intended to provide anonymity, hide traffic, or
bypass organizational, regulatory, or legal controls. Untrusted proxies are
outside the supported threat model.

In other words, if untrusted data passing through undici to the application
can trigger actions other than those documented for the APIs, there is likely
Expand Down Expand Up @@ -172,6 +183,16 @@ lead to a loss of confidentiality, integrity, or availability.
input to request options) are the application's responsibility, not
vulnerabilities in undici.

#### Unauthorized or untrusted proxy use

* If an application configures undici to use a proxy that is untrusted,
malicious, or not authorized by the relevant network or deployment authority,
any resulting privacy loss, policy bypass, regulatory exposure, traffic
manipulation, or unavailability is the deployment's responsibility. Undici
cannot determine whether a proxy is authorized for a user's network or
jurisdiction, and preventing use of proxies to hide traffic or evade controls
is a non-goal.

## Receiving security updates

Security notifications will be distributed via
Expand Down
4 changes: 4 additions & 0 deletions deps/undici/src/docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ added: v1.0.0
* `headersTimeout` {number|null} The timeout, in milliseconds, the parser
waits to receive the complete HTTP headers before the request times out. Use
`0` to disable it entirely. **Default:** `300e3`.
HTTP/1.1 headers/body parser timeouts are not guaranteed to fire with exact
millisecond precision: delays up to 1000ms use native timers, while larger
delays use undici's lower-overhead fast timers with a target resolution
around 500ms.
* `connectTimeout` {number|null} The timeout, in milliseconds, for
establishing a socket connection. Use `0` to disable it entirely.
**Default:** `10e3`.
Expand Down
6 changes: 6 additions & 0 deletions deps/undici/src/docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ added: v4.0.0
* `bodyTimeout` {number|null} The time, in milliseconds, after which the request
times out while receiving body data. Monitors the time between body chunks.
Use `0` to disable it entirely. Defaults to 300 seconds.
HTTP/1.1 headers/body parser timeouts are not guaranteed to fire with exact
millisecond precision: delays up to 1000ms use native timers, while larger
delays use undici's lower-overhead fast timers with a target resolution
around 500ms.
* `reset` {boolean} Whether the request should establish a keep-alive
connection. **Default:** `false`.
* `expectContinue` {boolean} For HTTP/2, appends the `expect: 100-continue`
Expand Down Expand Up @@ -729,6 +733,8 @@ await client.request({ path: '/', method: 'GET' })
```

For the full list of built-in interceptors provided by undici, see [Interceptors](Interceptors.md).
For an example of a custom interceptor that wraps handler callbacks, see
[Custom interceptors](Interceptors.md#custom-interceptors).

### Event: `'connect'`

Expand Down
59 changes: 59 additions & 0 deletions deps/undici/src/docs/docs/api/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,65 @@ const client = new Client('https://example.com').compose(

---

## Custom interceptors

Custom interceptors use the same shape as
[`dispatcher.compose()`](./Dispatcher.md#dispatchercomposeinterceptors-interceptor):
an interceptor takes a `dispatch` function and returns another dispatch-like
function with the same `(options, handler)` signature.

When an interceptor wraps the handler, forward the callbacks that it does not
handle itself. The complete handler callback list is documented under
[`dispatcher.dispatch(options, handler)`](./Dispatcher.md#dispatcherdispatchoptions-handler).

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

const timingInterceptor = dispatch => {
return (options, handler) => {
const started = performance.now()

return dispatch(options, {
...handler,
onResponseStart (controller, statusCode, headers, statusMessage) {
const duration = Math.round(performance.now() - started)
const method = options.method ?? 'GET'
const origin = options.origin ?? ''

console.log(`${method} ${origin}${options.path} -> ${statusCode} in ${duration}ms`)

return handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
},
onResponseError (controller, error) {
const duration = Math.round(performance.now() - started)

console.error(`request failed after ${duration}ms`, error)

return handler.onResponseError?.(controller, error)
}
})
}
}

const dispatcher = new Agent().compose(timingInterceptor)

const { body } = await dispatcher.request({
origin: 'https://example.com',
path: '/',
method: 'GET'
})

await body.dump()
await dispatcher.close()
```
---
## `interceptors.dump([opts])`
Reads and discards the response body up to a configurable size limit. Useful
Expand Down
27 changes: 27 additions & 0 deletions deps/undici/src/docs/docs/api/MockAgent.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ registered on the {MockClient} or {MockPool} instances returned by
`MockAgent` is set as the dispatcher (for example through
[`setGlobalDispatcher()`][] or a per-request `dispatcher` option).

> [!NOTE]
> [`setGlobalDispatcher()`][] only affects undici APIs that use the global
> dispatcher, such as `request()` and `fetch()`. It does not replace or
> monkeypatch {Pool} or {Client} instances that were created separately. To test
> code that accepts or creates a pool/client directly, pass the {MockPool} or
> {MockClient} returned by [`mockAgent.get(origin)`][] into that code.

```mjs
import { MockAgent } from 'undici'

Expand Down Expand Up @@ -139,6 +146,26 @@ for await (const data of body) {
}
```

```mjs displayName="Testing code that accepts a pool"
import { MockAgent } from 'undici'

async function getStatus (pool) {
const { statusCode } = await pool.request({
path: '/foo',
method: 'GET'
})

return statusCode
}

const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200)

console.log(await getStatus(mockPool)) // 200
```

```mjs displayName="Returning a MockClient"
import { MockAgent, request } from 'undici'

Expand Down
29 changes: 28 additions & 1 deletion deps/undici/src/docs/docs/api/MockPool.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ The reply behaviour of a matching request is defined through the returned
computing all reply options dynamically rather than just the body.
* `callback` {Function} A `(opts: MockResponseCallbackOptions) =>
{ statusCode, data, responseOptions }` function invoked with the incoming
request.
request. The callback may be asynchronous; a returned promise is awaited
and must resolve to the same shape.
* Returns: {MockScope}
* `replyWithError(error)` {Function} Defines an error for a matching request to
throw.
Expand Down Expand Up @@ -263,6 +264,32 @@ for await (const data of body) {
}
```

```mjs displayName="Reply with an asynchronous options callback"
import { readFile } from 'node:fs/promises'
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({
path: '/fixture',
method: 'GET'
}).reply(async ({ path }) => ({
statusCode: 200,
data: await readFile(new URL('./fixture.json', import.meta.url))
}))

const { statusCode, body } = await request('http://localhost:3000/fixture')

console.log('response received', statusCode) // response received 200

for await (const data of body) {
console.log('data', data.toString('utf8')) // contents of fixture.json
}
```

```mjs displayName="Multiple intercepts"
import { MockAgent, setGlobalDispatcher, request } from 'undici'

Expand Down
5 changes: 5 additions & 0 deletions deps/undici/src/docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ For more information about their behavior, please reference the body mixin from

This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.

For the top-level APIs below, the `url` argument supplies the request origin and
path. Do not pass `origin` or `path` in the second `options` argument. The linked
`Dispatcher` option types include those fields because dispatcher methods are
lower-level APIs that do not receive a separate `url` argument.

### `undici.request([url, options])`

* `url` {string|URL|UrlObject}
Expand Down
63 changes: 54 additions & 9 deletions deps/undici/src/lib/cache/memory-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,17 +218,62 @@ class MemoryCacheStore extends EventEmitter {
}

function findEntry (key, entries, now) {
return entries.find((entry) => (
entry.deleteAt > now &&
entry.method === key.method &&
(entry.vary == null || Object.keys(entry.vary).every(headerName => {
if (entry.vary[headerName] === null) {
return key.headers[headerName] === undefined
for (let i = 0; i < entries.length; i++) {
const entry = entries[i]
if (
entry.deleteAt > now &&
entry.method === key.method &&
varyMatches(key, entry)
) {
return entry
}
}
}

function varyMatches (key, entry) {
if (entry.vary == null) {
return true
}

for (const headerName in entry.vary) {
if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) {
return false
}
}

return true
}

/**
* @param {string|string[]|null|undefined} lhs
* @param {string|string[]|null|undefined} rhs
* @returns {boolean}
*/
function headerValueEquals (lhs, rhs) {
if (lhs == null && rhs == null) {
return true
}

if ((lhs == null && rhs != null) ||
(lhs != null && rhs == null)) {
return false
}

if (Array.isArray(lhs) && Array.isArray(rhs)) {
if (lhs.length !== rhs.length) {
return false
}

for (let i = 0; i < lhs.length; i++) {
if (lhs[i] !== rhs[i]) {
return false
}
}

return true
}

return entry.vary[headerName] === key.headers[headerName]
}))
))
return lhs === rhs
}

module.exports = MemoryCacheStore
8 changes: 7 additions & 1 deletion deps/undici/src/lib/cache/sqlite-cache-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,13 @@ function headerValueEquals (lhs, rhs) {
return false
}

return lhs.every((x, i) => x === rhs[i])
for (let i = 0; i < lhs.length; i++) {
if (lhs[i] !== rhs[i]) {
return false
}
}

return true
}

return lhs === rhs
Expand Down
15 changes: 13 additions & 2 deletions deps/undici/src/lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ class Request {

this.method = method

this.typeOfService = typeOfService ?? 0
this.typeOfService = typeOfService

this.abort = null

Expand Down Expand Up @@ -472,7 +472,13 @@ function processHeader (request, key, val) {
} else if (typeof val[i] === 'object') {
throw new InvalidArgumentError(`invalid ${key} header`)
} else {
arr.push(`${val[i]}`)
// Coerce primitives (and reject unsafe coercions such as functions
// with a crafted toString/Symbol.toPrimitive).
const str = `${val[i]}`
if (!isValidHeaderValue(str)) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
arr.push(str)
}
}
val = arr
Expand All @@ -483,7 +489,12 @@ function processHeader (request, key, val) {
} else if (val === null) {
val = ''
} else {
// Coerce primitives (and reject unsafe coercions such as functions
// with a crafted toString/Symbol.toPrimitive).
val = `${val}`
if (!isValidHeaderValue(val)) {
throw new InvalidArgumentError(`invalid ${key} header`)
}
}

if (headerName === 'host') {
Expand Down
7 changes: 6 additions & 1 deletion deps/undici/src/lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,12 @@ function destroy (stream, err) {
stream.socket = null
}

stream.destroy(err)
try {
stream.destroy(err)
} catch {
// stream.destroy may throw on managed sockets (e.g., http2).
// Silently ignore — the socket lifecycle is handled by the subsystem.
}
} else if (err) {
queueMicrotask(() => {
stream.emit('error', err)
Expand Down
Loading
Loading