deps: update undici to 7.13.0

PR-URL: https://github.com/nodejs/node/pull/59338
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Node.js GitHub Bot 2025-08-05 18:00:31 +01:00 committed by GitHub
parent 4f5d11e6fb
commit 3b715d3544
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1924 additions and 364 deletions

View file

@ -440,13 +440,14 @@ This behavior is intentional for server-side environments where CORS restriction
* https://fetch.spec.whatwg.org/#garbage-collection
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body.
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources.
Garbage collection in Node is less aggressive and deterministic
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
which means that leaving the release of connection resources to the garbage collector can lead
to excessive connection usage, reduced performance (due to less connection re-use), and even
stalls or deadlocks when running out of connections.
Therefore, __it is important to always either consume or cancel the response body anyway__.
```js
// Do
@ -459,7 +460,15 @@ for await (const chunk of body) {
const { headers } = await fetch(url);
```
The same applies for `request` too:
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
```js
const headers = await fetch(url, { method: 'HEAD' })
.then(res => res.headers)
```
Note that consuming the response body is _mandatory_ for `request`:
```js
// Do
const { body, headers } = await request(url);
@ -469,13 +478,6 @@ await res.body.dump(); // force consumption of body
const { headers } = await request(url);
```
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
```js
const headers = await fetch(url, { method: 'HEAD' })
.then(res => res.headers)
```
#### Forbidden and Safelisted Header Names
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name

View file

@ -27,7 +27,7 @@ For detailed information on the parsing process and potential validation errors,
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
* **proxyTunnel** `boolean` (optional) - By default, ProxyAgent will request that the Proxy facilitate a tunnel between the endpoint and the agent. Setting `proxyTunnel` to false avoids issuing a CONNECT extension, and includes the endpoint domain and path in each request.
* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address.
Examples:

View file

@ -0,0 +1,616 @@
# SnapshotAgent
The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses.
## Use Cases
- **Integration Testing**: Record real API interactions and replay them in tests
- **Offline Development**: Work with APIs without network connectivity
- **Consistent Test Data**: Ensure tests use the same responses across runs
- **API Contract Testing**: Capture and validate API behavior over time
## Constructor
```javascript
new SnapshotAgent([options])
```
### Parameters
- **options** `Object` (optional)
- **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'`
- **snapshotPath** `String` - Path to the snapshot file for loading/saving
- **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity`
- **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false`
- **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000`
- **matchHeaders** `Array<String>` - Specific headers to include in request matching. Default: all headers
- **ignoreHeaders** `Array<String>` - Headers to ignore during request matching
- **excludeHeaders** `Array<String>` - Headers to exclude from snapshots (for security)
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
- **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback
- All other options from `MockAgent` are supported
### Modes
#### Record Mode (`'record'`)
Makes real HTTP requests and saves the responses to snapshots.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Makes real requests and records them
const response = await fetch('https://api.example.com/users')
const users = await response.json()
// Save recorded snapshots
await agent.saveSnapshots()
```
#### Playback Mode (`'playback'`)
Replays recorded responses without making real HTTP requests.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Uses recorded response instead of real request
const response = await fetch('https://api.example.com/users')
```
#### Update Mode (`'update'`)
Uses existing snapshots when available, but records new ones for missing requests.
```javascript
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({
mode: 'update',
snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)
// Uses snapshot if exists, otherwise makes real request and records it
const response = await fetch('https://api.example.com/new-endpoint')
```
## Instance Methods
### `agent.saveSnapshots([filePath])`
Saves all recorded snapshots to a file.
#### Parameters
- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided.
#### Returns
`Promise<void>`
```javascript
await agent.saveSnapshots('./custom-snapshots.json')
```
## Advanced Configuration
### Header Filtering
Control which headers are used for request matching and what gets stored in snapshots:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Only match these specific headers
matchHeaders: ['content-type', 'accept'],
// Ignore these headers during matching (but still store them)
ignoreHeaders: ['user-agent', 'date'],
// Exclude sensitive headers from snapshots entirely
excludeHeaders: ['authorization', 'x-api-key', 'cookie']
})
```
### Custom Request/Response Filtering
Use callback functions to determine what gets recorded or played back:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Only record GET requests to specific endpoints
shouldRecord: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
},
// Skip authentication endpoints during playback
shouldPlayback: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
return !url.pathname.includes('/auth/')
}
})
```
### URL Pattern Exclusion
Exclude specific URLs from recording/playback using patterns:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
excludeUrls: [
'https://analytics.example.com', // String match
/\/api\/v\d+\/health/, // Regex pattern
'telemetry' // Substring match
]
})
```
### Memory Management
Configure automatic memory and disk management:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Keep only 1000 snapshots in memory
maxSnapshots: 1000,
// Automatically save to disk every 30 seconds
autoFlush: true,
flushInterval: 30000
})
```
### Sequential Response Handling
Handle multiple responses for the same request (similar to nock):
```javascript
// In record mode, multiple identical requests get recorded as separate responses
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' })
// First call returns response A
await fetch('https://api.example.com/random')
// Second call returns response B
await fetch('https://api.example.com/random')
await agent.saveSnapshots()
// In playback mode, calls return responses in sequence
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' })
// Returns response A
const first = await fetch('https://api.example.com/random')
// Returns response B
const second = await fetch('https://api.example.com/random')
// Third call repeats the last response (B)
const third = await fetch('https://api.example.com/random')
```
## Managing Snapshots
### Replacing Existing Snapshots
```javascript
// Load existing snapshots
await agent.loadSnapshots('./old-snapshots.json')
// Get snapshot data
const recorder = agent.getRecorder()
const snapshots = recorder.getSnapshots()
// Modify or filter snapshots
const filteredSnapshots = snapshots.filter(s =>
!s.request.url.includes('deprecated')
)
// Replace all snapshots
agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({
hash: `new-hash-${index}`,
snapshot
})))
// Save updated snapshots
await agent.saveSnapshots('./updated-snapshots.json')
```
### `agent.loadSnapshots([filePath])`
Loads snapshots from a file.
#### Parameters
- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided.
#### Returns
`Promise<void>`
```javascript
await agent.loadSnapshots('./existing-snapshots.json')
```
### `agent.getRecorder()`
Gets the underlying `SnapshotRecorder` instance.
#### Returns
`SnapshotRecorder`
```javascript
const recorder = agent.getRecorder()
console.log(`Recorded ${recorder.size()} interactions`)
```
### `agent.getMode()`
Gets the current snapshot mode.
#### Returns
`String` - The current mode (`'record'`, `'playback'`, or `'update'`)
### `agent.clearSnapshots()`
Clears all recorded snapshots from memory.
```javascript
agent.clearSnapshots()
```
## Working with Different Request Types
### GET Requests
```javascript
// Record mode
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' })
setGlobalDispatcher(agent)
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const post = await response.json()
await agent.saveSnapshots()
```
### POST Requests with Body
```javascript
// Record mode
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' })
setGlobalDispatcher(agent)
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test Post', body: 'Content' })
})
await agent.saveSnapshots()
```
### Using with `undici.request`
SnapshotAgent works with all undici APIs, not just fetch:
```javascript
import { SnapshotAgent, request, setGlobalDispatcher } from 'undici'
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' })
setGlobalDispatcher(agent)
const { statusCode, headers, body } = await request('https://api.example.com/data')
const data = await body.json()
await agent.saveSnapshots()
```
## Test Integration
### Basic Test Setup
```javascript
import { test } from 'node:test'
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
test('API integration test', async (t) => {
const originalDispatcher = getGlobalDispatcher()
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/api-test.json'
})
setGlobalDispatcher(agent)
t.after(() => setGlobalDispatcher(originalDispatcher))
// This will use recorded data
const response = await fetch('https://api.example.com/users')
const users = await response.json()
assert(Array.isArray(users))
assert(users.length > 0)
})
```
### Environment-Based Mode Selection
```javascript
const mode = process.env.SNAPSHOT_MODE || 'playback'
const agent = new SnapshotAgent({
mode,
snapshotPath: './test/snapshots/integration.json'
})
// Run with: SNAPSHOT_MODE=record npm test (to record)
// Run with: npm test (to playback)
```
### Test Helper Function
```javascript
function createSnapshotAgent(testName, mode = 'playback') {
return new SnapshotAgent({
mode,
snapshotPath: `./test/snapshots/${testName}.json`
})
}
test('user API test', async (t) => {
const agent = createSnapshotAgent('user-api')
setGlobalDispatcher(agent)
// Test implementation...
})
```
## Snapshot File Format
Snapshots are stored as JSON with the following structure:
```json
[
{
"hash": "dGVzdC1oYXNo...",
"snapshot": {
"request": {
"method": "GET",
"url": "https://api.example.com/users",
"headers": {
"authorization": "Bearer token"
},
"body": undefined
},
"response": {
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded
"trailers": {}
},
"timestamp": "2024-01-01T00:00:00.000Z"
}
}
]
```
## Security Considerations
### Sensitive Data in Snapshots
By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information:
```javascript
const agent = new SnapshotAgent({
mode: 'record',
snapshotPath: './snapshots.json',
// Exclude sensitive headers from snapshots
excludeHeaders: [
'authorization',
'x-api-key',
'cookie',
'set-cookie',
'x-auth-token',
'x-csrf-token'
],
// Filter out requests with sensitive data
shouldRecord: (requestOpts) => {
const url = new URL(requestOpts.path, requestOpts.origin)
// Don't record authentication endpoints
if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) {
return false
}
// Don't record if request contains sensitive body data
if (requestOpts.body && typeof requestOpts.body === 'string') {
const body = requestOpts.body.toLowerCase()
if (body.includes('password') || body.includes('secret')) {
return false
}
}
return true
}
})
```
### Snapshot File Security
**Important**: Snapshot files may contain sensitive data. Handle them securely:
- ✅ Add snapshot files to `.gitignore` if they contain real API data
- ✅ Use environment-specific snapshots (dev/staging/prod)
- ✅ Regularly review snapshot contents for sensitive information
- ✅ Use the `excludeHeaders` option for production snapshots
- ❌ Never commit snapshots with real authentication tokens
- ❌ Don't share snapshot files containing personal data
```gitignore
# Exclude snapshots with real data
/test/snapshots/production-*.json
/test/snapshots/*-real-data.json
# Include sanitized test snapshots
!/test/snapshots/mock-*.json
```
## Error Handling
### Missing Snapshots in Playback Mode
```javascript
try {
const response = await fetch('https://api.example.com/nonexistent')
} catch (error) {
if (error.message.includes('No snapshot found')) {
// Handle missing snapshot
console.log('Snapshot not found for this request')
}
}
```
### Handling Network Errors in Record Mode
```javascript
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
try {
const response = await fetch('https://nonexistent-api.example.com/data')
} catch (error) {
// Network errors are not recorded as snapshots
console.log('Network error:', error.message)
}
```
## Best Practices
### 1. Organize Snapshots by Test Suite
```javascript
// Use descriptive snapshot file names
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json`
})
```
### 2. Version Control Snapshots
Add snapshot files to version control to ensure consistent test behavior across environments:
```gitignore
# Include snapshots in version control
!/test/snapshots/*.json
```
### 3. Clean Up Test Data
```javascript
test('API test', async (t) => {
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/temp-test.json'
})
// Clean up after test
t.after(() => {
agent.clearSnapshots()
})
})
```
### 4. Snapshot Validation
```javascript
test('validate snapshot contents', async (t) => {
const agent = new SnapshotAgent({
mode: 'playback',
snapshotPath: './test/snapshots/validation.json'
})
const recorder = agent.getRecorder()
const snapshots = recorder.getSnapshots()
// Validate snapshot structure
assert(snapshots.length > 0, 'Should have recorded snapshots')
assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS')
})
```
## Comparison with Other Tools
### vs Manual MockAgent Setup
**Manual MockAgent:**
```javascript
const mockAgent = new MockAgent()
const mockPool = mockAgent.get('https://api.example.com')
mockPool.intercept({
path: '/users',
method: 'GET'
}).reply(200, [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
])
```
**SnapshotAgent:**
```javascript
// Record once
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
// Real API call gets recorded automatically
// Use in tests
const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' })
// Automatically replays recorded response
```
### vs nock
SnapshotAgent provides similar functionality to nock but is specifically designed for undici:
- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.)
- ✅ Supports undici-specific features (RetryAgent, connection pooling)
- ✅ Better TypeScript integration
- ✅ More efficient for high-performance scenarios
## 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

View file

@ -18,6 +18,7 @@ const MockClient = require('./lib/mock/mock-client')
const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
const MockAgent = require('./lib/mock/mock-agent')
const MockPool = require('./lib/mock/mock-pool')
const SnapshotAgent = require('./lib/mock/snapshot-agent')
const mockErrors = require('./lib/mock/mock-errors')
const RetryHandler = require('./lib/handler/retry-handler')
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
@ -178,6 +179,7 @@ module.exports.MockCallHistory = MockCallHistory
module.exports.MockCallHistoryLog = MockCallHistoryLog
module.exports.MockPool = MockPool
module.exports.MockAgent = MockAgent
module.exports.SnapshotAgent = SnapshotAgent
module.exports.mockErrors = mockErrors
const { EventSource } = require('./lib/web/eventsource/eventsource')

View file

@ -1,5 +1,3 @@
// Ported from https://github.com/nodejs/undici/pull/907
'use strict'
const assert = require('node:assert')
@ -50,23 +48,32 @@ class BodyReadable extends Readable {
this[kAbort] = abort
/**
* @type {Consume | null}
*/
/** @type {Consume | null} */
this[kConsume] = null
/** @type {number} */
this[kBytesRead] = 0
/**
* @type {ReadableStream|null}
*/
/** @type {ReadableStream|null} */
this[kBody] = null
/** @type {boolean} */
this[kUsed] = false
/** @type {string} */
this[kContentType] = contentType
/** @type {number|null} */
this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
// Is stream being consumed through Readable API?
// This is an optimization so that we avoid checking
// for 'data' and 'readable' listeners in the hot path
// inside push().
/**
* Is stream being consumed through Readable API?
* This is an optimization so that we avoid checking
* for 'data' and 'readable' listeners in the hot path
* inside push().
*
* @type {boolean}
*/
this[kReading] = false
}
@ -96,7 +103,7 @@ class BodyReadable extends Readable {
}
/**
* @param {string} event
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
@ -109,7 +116,7 @@ class BodyReadable extends Readable {
}
/**
* @param {string} event
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
@ -147,12 +154,14 @@ class BodyReadable extends Readable {
* @returns {boolean}
*/
push (chunk) {
this[kBytesRead] += chunk ? chunk.length : 0
if (this[kConsume] && chunk !== null) {
if (chunk) {
this[kBytesRead] += chunk.length
if (this[kConsume]) {
consumePush(this[kConsume], chunk)
return this[kReading] ? super.push(chunk) : true
}
}
return super.push(chunk)
}
@ -338,9 +347,23 @@ function isUnusable (bodyReadable) {
return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
}
/**
* @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
*/
/**
* @template {ConsumeType} T
* @typedef {T extends 'text' ? string :
* T extends 'json' ? unknown :
* T extends 'blob' ? Blob :
* T extends 'arrayBuffer' ? ArrayBuffer :
* T extends 'bytes' ? Uint8Array :
* never
* } ConsumeReturnType
*/
/**
* @typedef {object} Consume
* @property {string} type
* @property {ConsumeType} type
* @property {BodyReadable} stream
* @property {((value?: any) => void)} resolve
* @property {((err: Error) => void)} reject
@ -349,9 +372,10 @@ function isUnusable (bodyReadable) {
*/
/**
* @template {ConsumeType} T
* @param {BodyReadable} stream
* @param {string} type
* @returns {Promise<any>}
* @param {T} type
* @returns {Promise<ConsumeReturnType<T>>}
*/
function consume (stream, type) {
assert(!stream[kConsume])
@ -361,9 +385,7 @@ function consume (stream, type) {
const rState = stream._readableState
if (rState.destroyed && rState.closeEmitted === false) {
stream
.on('error', err => {
reject(err)
})
.on('error', reject)
.on('close', () => {
reject(new TypeError('unusable'))
})
@ -438,7 +460,7 @@ function consumeStart (consume) {
/**
* @param {Buffer[]} chunks
* @param {number} length
* @param {BufferEncoding} encoding
* @param {BufferEncoding} [encoding='utf8']
* @returns {string}
*/
function chunksDecode (chunks, length, encoding) {

View file

@ -5,7 +5,6 @@ const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
const { IncomingMessage } = require('node:http')
const stream = require('node:stream')
const net = require('node:net')
const { Blob } = require('node:buffer')
const { stringify } = require('node:querystring')
const { EventEmitter: EE } = require('node:events')
const timers = require('../util/timers')

View file

@ -1,6 +1,6 @@
'use strict'
const { kProxy, kClose, kDestroy, kDispatch, kConnector } = require('../core/symbols')
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
const { URL } = require('node:url')
const Agent = require('./agent')
const Pool = require('./pool')
@ -27,61 +27,69 @@ function defaultFactory (origin, opts) {
const noop = () => {}
class ProxyClient extends DispatcherBase {
#client = null
constructor (origin, opts) {
if (typeof origin === 'string') {
origin = new URL(origin)
function defaultAgentFactory (origin, opts) {
if (opts.connections === 1) {
return new Client(origin, opts)
}
return new Pool(origin, opts)
}
if (origin.protocol !== 'http:' && origin.protocol !== 'https:') {
throw new InvalidArgumentError('ProxyClient only supports http and https protocols')
}
class Http1ProxyWrapper extends DispatcherBase {
#client
constructor (proxyUrl, { headers = {}, connect, factory }) {
super()
if (!proxyUrl) {
throw new InvalidArgumentError('Proxy URL is mandatory')
}
this.#client = new Client(origin, opts)
this[kProxyHeaders] = headers
if (factory) {
this.#client = factory(proxyUrl, { connect })
} else {
this.#client = new Client(proxyUrl, { connect })
}
}
[kDispatch] (opts, handler) {
const onHeaders = handler.onHeaders
handler.onHeaders = function (statusCode, data, resume) {
if (statusCode === 407) {
if (typeof handler.onError === 'function') {
handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
}
return
}
if (onHeaders) onHeaders.call(this, statusCode, data, resume)
}
// Rewrite request as an HTTP1 Proxy request, without tunneling.
const {
origin,
path = '/',
headers = {}
} = opts
opts.path = origin + path
if (!('host' in headers) && !('Host' in headers)) {
const { host } = new URL(origin)
headers.host = host
}
opts.headers = { ...this[kProxyHeaders], ...headers }
return this.#client[kDispatch](opts, handler)
}
async [kClose] () {
await this.#client.close()
return this.#client.close()
}
async [kDestroy] () {
await this.#client.destroy()
}
async [kDispatch] (opts, handler) {
const { method, origin } = opts
if (method === 'CONNECT') {
this.#client[kConnector]({
origin,
port: opts.port || defaultProtocolPort(opts.protocol),
path: opts.host,
signal: opts.signal,
headers: {
...this[kProxyHeaders],
host: opts.host
},
servername: this[kProxyTls]?.servername || opts.servername
},
(err, socket) => {
if (err) {
handler.callback(err)
} else {
handler.callback(null, { socket, statusCode: 200 })
}
}
)
return
}
if (typeof origin === 'string') {
opts.origin = new URL(origin)
}
return this.#client.dispatch(opts, handler)
async [kDestroy] (err) {
return this.#client.destroy(err)
}
}
class ProxyAgent extends DispatcherBase {
constructor (opts) {
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@ -104,6 +112,7 @@ class ProxyAgent extends DispatcherBase {
this[kRequestTls] = opts.requestTls
this[kProxyTls] = opts.proxyTls
this[kProxyHeaders] = opts.headers || {}
this[kTunnelProxy] = proxyTunnel
if (opts.auth && opts.token) {
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
@ -116,21 +125,25 @@ class ProxyAgent extends DispatcherBase {
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
}
const factory = (!proxyTunnel && protocol === 'http:')
? (origin, options) => {
if (origin.protocol === 'http:') {
return new ProxyClient(origin, options)
}
return new Client(origin, options)
}
: undefined
const connect = buildConnector({ ...opts.proxyTls })
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
this[kClient] = clientFactory(url, { connect, factory })
this[kTunnelProxy] = proxyTunnel
const agentFactory = opts.factory || defaultAgentFactory
const factory = (origin, options) => {
const { protocol } = new URL(origin)
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
return new Http1ProxyWrapper(this[kProxy].uri, {
headers: this[kProxyHeaders],
connect,
factory: agentFactory
})
}
return agentFactory(origin, options)
}
this[kClient] = clientFactory(url, { connect })
this[kAgent] = new Agent({
...opts,
factory,
connect: async (opts, callback) => {
let requestedPath = opts.host
if (!opts.port) {
@ -185,10 +198,6 @@ class ProxyAgent extends DispatcherBase {
headers.host = host
}
if (!this.#shouldConnect(new URL(opts.origin))) {
opts.path = opts.origin + opts.path
}
return this[kAgent].dispatch(
{
...opts,
@ -221,19 +230,6 @@ class ProxyAgent extends DispatcherBase {
await this[kAgent].destroy()
await this[kClient].destroy()
}
#shouldConnect (uri) {
if (typeof uri === 'string') {
uri = new URL(uri)
}
if (this[kTunnelProxy]) {
return true
}
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
return true
}
return false
}
}
/**

View file

@ -133,6 +133,16 @@ class RedirectHandler {
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
const path = search ? `${pathname}${search}` : pathname
// Check for redirect loops by seeing if we've already visited this URL in our history
// This catches the case where Client/Pool try to handle cross-origin redirects but fail
// and keep redirecting to the same URL in an infinite loop
const redirectUrlString = `${origin}${path}`
for (const historyUrl of this.history) {
if (historyUrl.toString() === redirectUrlString) {
throw new InvalidArgumentError(`Redirect loop detected. Cannot redirect to ${origin}. This typically happens when using a Client or Pool with cross-origin redirects. Use an Agent for cross-origin redirects.`)
}
}
// Remove headers referring to the original URL.
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
// https://tools.ietf.org/html/rfc7231#section-6.4

View file

@ -57,7 +57,8 @@ class DumpHandler extends DecoratorHandler {
return
}
err = this.#controller.reason ?? err
// On network errors before connect, controller will be null
err = this.#controller?.reason ?? err
super.onResponseError(controller, err)
}

View file

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

View file

@ -17,7 +17,8 @@ const {
kMockAgentAddCallHistoryLog,
kMockAgentMockCallHistoryInstance,
kMockAgentAcceptsNonStandardSearchParameters,
kMockCallHistoryAddLog
kMockCallHistoryAddLog,
kIgnoreTrailingSlash
} = require('./mock-symbols')
const MockClient = require('./mock-client')
const MockPool = require('./mock-pool')
@ -37,6 +38,7 @@ class MockAgent extends Dispatcher {
this[kIsMockActive] = true
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false
this[kIgnoreTrailingSlash] = mockOptions?.ignoreTrailingSlash ?? false
// Instantiate Agent and encapsulate
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
@ -54,11 +56,15 @@ class MockAgent extends Dispatcher {
}
get (origin) {
let dispatcher = this[kMockAgentGet](origin)
const originKey = this[kIgnoreTrailingSlash]
? origin.replace(/\/$/, '')
: origin
let dispatcher = this[kMockAgentGet](originKey)
if (!dispatcher) {
dispatcher = this[kFactory](origin)
this[kMockAgentSet](origin, dispatcher)
dispatcher = this[kFactory](originKey)
this[kMockAgentSet](originKey, dispatcher)
}
return dispatcher
}

View file

@ -0,0 +1,333 @@
'use strict'
const Agent = require('../dispatcher/agent')
const MockAgent = require('./mock-agent')
const { SnapshotRecorder } = require('./snapshot-recorder')
const WrapHandler = require('../handler/wrap-handler')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
const kSnapshotRecorder = Symbol('kSnapshotRecorder')
const kSnapshotMode = Symbol('kSnapshotMode')
const kSnapshotPath = Symbol('kSnapshotPath')
const kSnapshotLoaded = Symbol('kSnapshotLoaded')
const kRealAgent = Symbol('kRealAgent')
// Static flag to ensure warning is only emitted once
let warningEmitted = false
class SnapshotAgent extends MockAgent {
constructor (opts = {}) {
// Emit experimental warning only once
if (!warningEmitted) {
process.emitWarning(
'SnapshotAgent is experimental and subject to change',
'ExperimentalWarning'
)
warningEmitted = true
}
const mockOptions = { ...opts }
delete mockOptions.mode
delete mockOptions.snapshotPath
super(mockOptions)
// Validate mode option
const validModes = ['record', 'playback', 'update']
const mode = opts.mode || 'record'
if (!validModes.includes(mode)) {
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be one of: ${validModes.join(', ')}`)
}
// Validate snapshotPath is provided when required
if ((mode === 'playback' || mode === 'update') && !opts.snapshotPath) {
throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)
}
this[kSnapshotMode] = mode
this[kSnapshotPath] = opts.snapshotPath
this[kSnapshotRecorder] = new SnapshotRecorder({
snapshotPath: this[kSnapshotPath],
mode: this[kSnapshotMode],
maxSnapshots: opts.maxSnapshots,
autoFlush: opts.autoFlush,
flushInterval: opts.flushInterval,
matchHeaders: opts.matchHeaders,
ignoreHeaders: opts.ignoreHeaders,
excludeHeaders: opts.excludeHeaders,
matchBody: opts.matchBody,
matchQuery: opts.matchQuery,
caseSensitive: opts.caseSensitive,
shouldRecord: opts.shouldRecord,
shouldPlayback: opts.shouldPlayback,
excludeUrls: opts.excludeUrls
})
this[kSnapshotLoaded] = false
// For recording/update mode, we need a real agent to make actual requests
if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update') {
this[kRealAgent] = new Agent(opts)
}
// Auto-load snapshots in playback/update mode
if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) {
this.loadSnapshots().catch(() => {
// Ignore load errors - file might not exist yet
})
}
}
dispatch (opts, handler) {
handler = WrapHandler.wrap(handler)
const mode = this[kSnapshotMode]
if (mode === 'playback' || mode === 'update') {
// Ensure snapshots are loaded
if (!this[kSnapshotLoaded]) {
// Need to load asynchronously, delegate to async version
return this._asyncDispatch(opts, handler)
}
// Try to find existing snapshot (synchronous)
const snapshot = this[kSnapshotRecorder].findSnapshot(opts)
if (snapshot) {
// Use recorded response (synchronous)
return this._replaySnapshot(snapshot, handler)
} else if (mode === 'update') {
// Make real request and record it (async required)
return this._recordAndReplay(opts, handler)
} else {
// Playback mode but no snapshot found
const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)
if (handler.onError) {
handler.onError(error)
return
}
throw error
}
} else if (mode === 'record') {
// Record mode - make real request and save response (async required)
return this._recordAndReplay(opts, handler)
} else {
throw new InvalidArgumentError(`Invalid snapshot mode: ${mode}. Must be 'record', 'playback', or 'update'`)
}
}
/**
* Async version of dispatch for when we need to load snapshots first
*/
async _asyncDispatch (opts, handler) {
await this.loadSnapshots()
return this.dispatch(opts, handler)
}
/**
* Records a real request and replays the response
*/
_recordAndReplay (opts, handler) {
const responseData = {
statusCode: null,
headers: {},
trailers: {},
body: []
}
const self = this // Capture 'this' context for use within nested handler callbacks
const recordingHandler = {
onRequestStart (controller, context) {
return handler.onRequestStart(controller, { ...context, history: this.history })
},
onRequestUpgrade (controller, statusCode, headers, socket) {
return handler.onRequestUpgrade(controller, statusCode, headers, socket)
},
onResponseStart (controller, statusCode, headers, statusMessage) {
responseData.statusCode = statusCode
responseData.headers = headers
return handler.onResponseStart(controller, statusCode, headers, statusMessage)
},
onResponseData (controller, chunk) {
responseData.body.push(chunk)
return handler.onResponseData(controller, chunk)
},
onResponseEnd (controller, trailers) {
responseData.trailers = trailers
// Record the interaction using captured 'self' context (fire and forget)
const responseBody = Buffer.concat(responseData.body)
self[kSnapshotRecorder].record(opts, {
statusCode: responseData.statusCode,
headers: responseData.headers,
body: responseBody,
trailers: responseData.trailers
}).then(() => {
handler.onResponseEnd(controller, trailers)
}).catch((error) => {
handler.onResponseError(controller, error)
})
}
}
// Use composed agent if available (includes interceptors), otherwise use real agent
const agent = this[kRealAgent]
return agent.dispatch(opts, recordingHandler)
}
/**
* Replays a recorded response
*/
_replaySnapshot (snapshot, handler) {
return new Promise((resolve) => {
// Simulate the response
setImmediate(() => {
try {
const { response } = snapshot
const controller = {
pause () {},
resume () {},
abort (reason) {
this.aborted = true
this.reason = reason
},
aborted: false,
paused: false
}
handler.onRequestStart(controller)
handler.onResponseStart(controller, response.statusCode, response.headers)
// Body is always stored as base64 string
const body = Buffer.from(response.body, 'base64')
handler.onResponseData(controller, body)
handler.onResponseEnd(controller, response.trailers)
resolve()
} catch (error) {
handler.onError?.(error)
}
})
})
}
/**
* Loads snapshots from file
*/
async loadSnapshots (filePath) {
await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])
this[kSnapshotLoaded] = true
// In playback mode, set up MockAgent interceptors for all snapshots
if (this[kSnapshotMode] === 'playback') {
this._setupMockInterceptors()
}
}
/**
* Saves snapshots to file
*/
async saveSnapshots (filePath) {
return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])
}
/**
* Sets up MockAgent interceptors based on recorded snapshots.
*
* This method creates MockAgent interceptors for each recorded snapshot,
* allowing the SnapshotAgent to fall back to MockAgent's standard intercept
* mechanism in playback mode. Each interceptor is configured to persist
* (remain active for multiple requests) and responds with the recorded
* response data.
*
* Called automatically when loading snapshots in playback mode.
*
* @private
*/
_setupMockInterceptors () {
for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {
const { request, responses, response } = snapshot
const url = new URL(request.url)
const mockPool = this.get(url.origin)
// Handle both new format (responses array) and legacy format (response object)
const responseData = responses ? responses[0] : response
if (!responseData) continue
mockPool.intercept({
path: url.pathname + url.search,
method: request.method,
headers: request.headers,
body: request.body
}).reply(responseData.statusCode, responseData.body, {
headers: responseData.headers,
trailers: responseData.trailers
}).persist()
}
}
/**
* Gets the snapshot recorder
*/
getRecorder () {
return this[kSnapshotRecorder]
}
/**
* Gets the current mode
*/
getMode () {
return this[kSnapshotMode]
}
/**
* Clears all snapshots
*/
clearSnapshots () {
this[kSnapshotRecorder].clear()
}
/**
* Resets call counts for all snapshots (useful for test cleanup)
*/
resetCallCounts () {
this[kSnapshotRecorder].resetCallCounts()
}
/**
* Deletes a specific snapshot by request options
*/
deleteSnapshot (requestOpts) {
return this[kSnapshotRecorder].deleteSnapshot(requestOpts)
}
/**
* Gets information about a specific snapshot
*/
getSnapshotInfo (requestOpts) {
return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)
}
/**
* Replaces all snapshots with new data (full replacement)
*/
replaceSnapshots (snapshotData) {
this[kSnapshotRecorder].replaceSnapshots(snapshotData)
}
async close () {
// Close recorder (saves snapshots and cleans up timers)
await this[kSnapshotRecorder].close()
await this[kRealAgent]?.close()
await super.close()
}
}
module.exports = SnapshotAgent

View file

@ -0,0 +1,517 @@
'use strict'
const { writeFile, readFile, mkdir } = require('node:fs/promises')
const { dirname, resolve } = require('node:path')
const { InvalidArgumentError, UndiciError } = require('../core/errors')
/**
* Formats a request for consistent snapshot storage
* Caches normalized headers to avoid repeated processing
*/
function formatRequestKey (opts, cachedSets, matchOptions = {}) {
const url = new URL(opts.path, opts.origin)
// Cache normalized headers if not already done
const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
if (!opts._normalizedHeaders) {
opts._normalizedHeaders = normalized
}
return {
method: opts.method || 'GET',
url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
headers: filterHeadersForMatching(normalized, cachedSets, matchOptions),
body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : undefined
}
}
/**
* Filters headers based on matching configuration
*/
function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) {
if (!headers || typeof headers !== 'object') return {}
const {
matchHeaders = null,
caseSensitive = false
} = matchOptions
const filtered = {}
const { ignoreSet, excludeSet, matchSet } = cachedSets
for (const [key, value] of Object.entries(headers)) {
const headerKey = caseSensitive ? key : key.toLowerCase()
// Skip if in exclude list (for security)
if (excludeSet.has(headerKey)) continue
// Skip if in ignore list (for matching)
if (ignoreSet.has(headerKey)) continue
// If matchHeaders is specified, only include those headers
if (matchHeaders && Array.isArray(matchHeaders)) {
if (!matchSet.has(headerKey)) continue
}
filtered[headerKey] = value
}
return filtered
}
/**
* Filters headers for storage (only excludes sensitive headers)
*/
function filterHeadersForStorage (headers, matchOptions = {}) {
if (!headers || typeof headers !== 'object') return {}
const {
excludeHeaders = [],
caseSensitive = false
} = matchOptions
const filtered = {}
const excludeSet = new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
for (const [key, value] of Object.entries(headers)) {
const headerKey = caseSensitive ? key : key.toLowerCase()
// Skip if in exclude list (for security)
if (excludeSet.has(headerKey)) continue
filtered[headerKey] = value
}
return filtered
}
/**
* Creates cached header sets for performance
*/
function createHeaderSetsCache (matchOptions = {}) {
const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = null, caseSensitive = false } = matchOptions
return {
ignoreSet: new Set(ignoreHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
excludeSet: new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
matchSet: matchHeaders && Array.isArray(matchHeaders)
? new Set(matchHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
: null
}
}
/**
* Normalizes headers for consistent comparison
*/
function normalizeHeaders (headers) {
if (!headers) return {}
const normalized = {}
// Handle array format (undici internal format: [name, value, name, value, ...])
if (Array.isArray(headers)) {
for (let i = 0; i < headers.length; i += 2) {
const key = headers[i]
const value = headers[i + 1]
if (key && value !== undefined) {
// Convert Buffers to strings if needed
const keyStr = Buffer.isBuffer(key) ? key.toString() : String(key)
const valueStr = Buffer.isBuffer(value) ? value.toString() : String(value)
normalized[keyStr.toLowerCase()] = valueStr
}
}
return normalized
}
// Handle object format
if (headers && typeof headers === 'object') {
for (const [key, value] of Object.entries(headers)) {
if (key && typeof key === 'string') {
normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
}
}
}
return normalized
}
/**
* Creates a hash key for request matching
*/
function createRequestHash (request) {
const parts = [
request.method,
request.url,
JSON.stringify(request.headers, Object.keys(request.headers).sort()),
request.body || ''
]
return Buffer.from(parts.join('|')).toString('base64url')
}
/**
* Checks if a URL matches any of the exclude patterns
*/
function isUrlExcluded (url, excludePatterns = []) {
if (!excludePatterns.length) return false
for (const pattern of excludePatterns) {
if (typeof pattern === 'string') {
// Simple string match (case-insensitive)
if (url.toLowerCase().includes(pattern.toLowerCase())) {
return true
}
} else if (pattern instanceof RegExp) {
// Regex pattern match
if (pattern.test(url)) {
return true
}
}
}
return false
}
class SnapshotRecorder {
constructor (options = {}) {
this.snapshots = new Map()
this.snapshotPath = options.snapshotPath
this.mode = options.mode || 'record'
this.loaded = false
this.maxSnapshots = options.maxSnapshots || Infinity
this.autoFlush = options.autoFlush || false
this.flushInterval = options.flushInterval || 30000 // 30 seconds default
this._flushTimer = null
this._flushTimeout = null
// Matching configuration
this.matchOptions = {
matchHeaders: options.matchHeaders || null, // null means match all headers
ignoreHeaders: options.ignoreHeaders || [],
excludeHeaders: options.excludeHeaders || [],
matchBody: options.matchBody !== false, // default: true
matchQuery: options.matchQuery !== false, // default: true
caseSensitive: options.caseSensitive || false
}
// Cache processed header sets to avoid recreating them on every request
this._headerSetsCache = createHeaderSetsCache(this.matchOptions)
// Request filtering callbacks
this.shouldRecord = options.shouldRecord || null // function(requestOpts) -> boolean
this.shouldPlayback = options.shouldPlayback || null // function(requestOpts) -> boolean
// URL pattern filtering
this.excludeUrls = options.excludeUrls || [] // Array of regex patterns or strings
// Start auto-flush timer if enabled
if (this.autoFlush && this.snapshotPath) {
this._startAutoFlush()
}
}
/**
* Records a request-response interaction
*/
async record (requestOpts, response) {
// Check if recording should be filtered out
if (this.shouldRecord && typeof this.shouldRecord === 'function') {
if (!this.shouldRecord(requestOpts)) {
return // Skip recording
}
}
// Check URL exclusion patterns
const url = new URL(requestOpts.path, requestOpts.origin).toString()
if (isUrlExcluded(url, this.excludeUrls)) {
return // Skip recording
}
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
const hash = createRequestHash(request)
// Extract response data - always store body as base64
const normalizedHeaders = normalizeHeaders(response.headers)
const responseData = {
statusCode: response.statusCode,
headers: filterHeadersForStorage(normalizedHeaders, this.matchOptions),
body: Buffer.isBuffer(response.body)
? response.body.toString('base64')
: Buffer.from(String(response.body || '')).toString('base64'),
trailers: response.trailers
}
// Remove oldest snapshot if we exceed maxSnapshots limit
if (this.snapshots.size >= this.maxSnapshots && !this.snapshots.has(hash)) {
const oldestKey = this.snapshots.keys().next().value
this.snapshots.delete(oldestKey)
}
// Support sequential responses - if snapshot exists, add to responses array
const existingSnapshot = this.snapshots.get(hash)
if (existingSnapshot && existingSnapshot.responses) {
existingSnapshot.responses.push(responseData)
existingSnapshot.timestamp = new Date().toISOString()
} else {
this.snapshots.set(hash, {
request,
responses: [responseData], // Always store as array for consistency
callCount: 0,
timestamp: new Date().toISOString()
})
}
// Auto-flush if enabled
if (this.autoFlush && this.snapshotPath) {
this._scheduleFlush()
}
}
/**
* Finds a matching snapshot for the given request
* Returns the appropriate response based on call count for sequential responses
*/
findSnapshot (requestOpts) {
// Check if playback should be filtered out
if (this.shouldPlayback && typeof this.shouldPlayback === 'function') {
if (!this.shouldPlayback(requestOpts)) {
return undefined // Skip playback
}
}
// Check URL exclusion patterns
const url = new URL(requestOpts.path, requestOpts.origin).toString()
if (isUrlExcluded(url, this.excludeUrls)) {
return undefined // Skip playback
}
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
const hash = createRequestHash(request)
const snapshot = this.snapshots.get(hash)
if (!snapshot) return undefined
// Handle sequential responses
if (snapshot.responses && Array.isArray(snapshot.responses)) {
const currentCallCount = snapshot.callCount || 0
const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
snapshot.callCount = currentCallCount + 1
return {
...snapshot,
response: snapshot.responses[responseIndex]
}
}
// Legacy format compatibility - convert single response to array format
if (snapshot.response && !snapshot.responses) {
snapshot.responses = [snapshot.response]
snapshot.callCount = 1
delete snapshot.response
return {
...snapshot,
response: snapshot.responses[0]
}
}
return snapshot
}
/**
* Loads snapshots from file
*/
async loadSnapshots (filePath) {
const path = filePath || this.snapshotPath
if (!path) {
throw new InvalidArgumentError('Snapshot path is required')
}
try {
const data = await readFile(resolve(path), 'utf8')
const parsed = JSON.parse(data)
// Convert array format back to Map
if (Array.isArray(parsed)) {
this.snapshots.clear()
for (const { hash, snapshot } of parsed) {
this.snapshots.set(hash, snapshot)
}
} else {
// Legacy object format
this.snapshots = new Map(Object.entries(parsed))
}
this.loaded = true
} catch (error) {
if (error.code === 'ENOENT') {
// File doesn't exist yet - that's ok for recording mode
this.snapshots.clear()
this.loaded = true
} else {
throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
}
}
}
/**
* Saves snapshots to file
*/
async saveSnapshots (filePath) {
const path = filePath || this.snapshotPath
if (!path) {
throw new InvalidArgumentError('Snapshot path is required')
}
const resolvedPath = resolve(path)
// Ensure directory exists
await mkdir(dirname(resolvedPath), { recursive: true })
// Convert Map to serializable format
const data = Array.from(this.snapshots.entries()).map(([hash, snapshot]) => ({
hash,
snapshot
}))
await writeFile(resolvedPath, JSON.stringify(data, null, 2), 'utf8', { flush: true })
}
/**
* Clears all recorded snapshots
*/
clear () {
this.snapshots.clear()
}
/**
* Gets all recorded snapshots
*/
getSnapshots () {
return Array.from(this.snapshots.values())
}
/**
* Gets snapshot count
*/
size () {
return this.snapshots.size
}
/**
* Resets call counts for all snapshots (useful for test cleanup)
*/
resetCallCounts () {
for (const snapshot of this.snapshots.values()) {
snapshot.callCount = 0
}
}
/**
* Deletes a specific snapshot by request options
*/
deleteSnapshot (requestOpts) {
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
const hash = createRequestHash(request)
return this.snapshots.delete(hash)
}
/**
* Gets information about a specific snapshot
*/
getSnapshotInfo (requestOpts) {
const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
const hash = createRequestHash(request)
const snapshot = this.snapshots.get(hash)
if (!snapshot) return null
return {
hash,
request: snapshot.request,
responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0),
callCount: snapshot.callCount || 0,
timestamp: snapshot.timestamp
}
}
/**
* Replaces all snapshots with new data (full replacement)
*/
replaceSnapshots (snapshotData) {
this.snapshots.clear()
if (Array.isArray(snapshotData)) {
for (const { hash, snapshot } of snapshotData) {
this.snapshots.set(hash, snapshot)
}
} else if (snapshotData && typeof snapshotData === 'object') {
// Legacy object format
this.snapshots = new Map(Object.entries(snapshotData))
}
}
/**
* Starts the auto-flush timer
*/
_startAutoFlush () {
if (!this._flushTimer) {
this._flushTimer = setInterval(() => {
this.saveSnapshots().catch(() => {
// Ignore flush errors - they shouldn't interrupt normal operation
})
}, this.flushInterval)
}
}
/**
* Stops the auto-flush timer
*/
_stopAutoFlush () {
if (this._flushTimer) {
clearInterval(this._flushTimer)
this._flushTimer = null
}
}
/**
* Schedules a flush (debounced to avoid excessive writes)
*/
_scheduleFlush () {
// Simple debouncing - clear existing timeout and set new one
if (this._flushTimeout) {
clearTimeout(this._flushTimeout)
}
this._flushTimeout = setTimeout(() => {
this.saveSnapshots().catch(() => {
// Ignore flush errors
})
this._flushTimeout = null
}, 1000) // 1 second debounce
}
/**
* Cleanup method to stop timers
*/
destroy () {
this._stopAutoFlush()
if (this._flushTimeout) {
clearTimeout(this._flushTimeout)
this._flushTimeout = null
}
}
/**
* Async close method that saves all recordings and performs cleanup
*/
async close () {
// Save any pending recordings if we have a snapshot path
if (this.snapshotPath && this.snapshots.size > 0) {
await this.saveSnapshots()
}
// Perform cleanup
this.destroy()
}
}
module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, isUrlExcluded, createHeaderSetsCache }

View file

@ -10,7 +10,6 @@ const {
} = require('./util')
const { FormData, setFormDataState } = require('./formdata')
const { webidl } = require('../webidl')
const { Blob } = require('node:buffer')
const assert = require('node:assert')
const { isErrored, isDisturbed } = require('node:stream')
const { isArrayBuffer } = require('node:util/types')

View file

@ -6,9 +6,6 @@ const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
const { makeEntry } = require('./formdata')
const { webidl } = require('../webidl')
const assert = require('node:assert')
const { File: NodeFile } = require('node:buffer')
const File = globalThis.File ?? NodeFile
const formDataNameBuffer = Buffer.from('form-data; name="')
const filenameBuffer = Buffer.from('filename')

View file

@ -3,12 +3,8 @@
const { iteratorMixin } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { webidl } = require('../webidl')
const { File: NativeFile } = require('node:buffer')
const nodeUtil = require('node:util')
/** @type {globalThis['File']} */
const File = globalThis.File ?? NativeFile
// https://xhr.spec.whatwg.org/#formdata
class FormData {
#state = []

View file

@ -509,7 +509,7 @@ webidl.is.USVString = function (value) {
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
webidl.is.File = webidl.util.MakeTypeAssertion(File)
webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)

249
deps/undici/src/package-lock.json generated vendored
View file

@ -1,12 +1,12 @@
{
"name": "undici",
"version": "7.12.0",
"version": "7.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "undici",
"version": "7.12.0",
"version": "7.13.0",
"license": "MIT",
"devDependencies": {
"@fastify/busboy": "3.1.1",
@ -261,14 +261,14 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.6"
"@babel/types": "^7.28.2"
},
"engines": {
"node": ">=6.9.0"
@ -564,9 +564,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1168,9 +1168,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
"version": "9.32.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
"dev": true,
"license": "MIT",
"engines": {
@ -1191,9 +1191,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz",
"integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -1959,9 +1959,9 @@
}
},
"node_modules/@reporters/github": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.7.2.tgz",
"integrity": "sha512-8mvTyKUxxDXkNIWfzv3FsHVwjr8JCwVtwidQws2neV6YgrsJW6OwTOBBhd01RKrDMXPxgpMQuFEfN9hRuUZGuA==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@reporters/github/-/github-1.8.0.tgz",
"integrity": "sha512-EJNbv7qvqbICrVbyaPLKWT/mGzdkkdskKuPg1hG0tVKeAEtH6D1gCZwZ84N/26CQ8FBsyfiUyVjwtgYEByGKWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2106,13 +2106,13 @@
}
},
"node_modules/@types/babel__traverse": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.20.7"
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/eslint": {
@ -2185,9 +2185,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.120",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.120.tgz",
"integrity": "sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==",
"version": "18.19.121",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.121.tgz",
"integrity": "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2236,17 +2236,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz",
"integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/type-utils": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/type-utils": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -2260,7 +2260,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.37.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -2276,16 +2276,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz",
"integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4"
},
"engines": {
@ -2301,14 +2301,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
"integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.37.0",
"@typescript-eslint/types": "^8.37.0",
"@typescript-eslint/tsconfig-utils": "^8.38.0",
"@typescript-eslint/types": "^8.38.0",
"debug": "^4.3.4"
},
"engines": {
@ -2323,14 +2323,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz",
"integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0"
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2341,9 +2341,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz",
"integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -2358,15 +2358,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz",
"integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -2383,9 +2383,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz",
"integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
"dev": true,
"license": "MIT",
"engines": {
@ -2397,16 +2397,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz",
"integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.37.0",
"@typescript-eslint/tsconfig-utils": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/visitor-keys": "8.37.0",
"@typescript-eslint/project-service": "8.38.0",
"@typescript-eslint/tsconfig-utils": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/visitor-keys": "8.38.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -2465,16 +2465,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz",
"integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.37.0",
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0"
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2489,13 +2489,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz",
"integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.37.0",
"@typescript-eslint/types": "8.38.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@ -3302,9 +3302,9 @@
}
},
"node_modules/babel-preset-current-node-syntax": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
"integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
"integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3325,7 +3325,7 @@
"@babel/plugin-syntax-top-level-await": "^7.14.5"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
"@babel/core": "^7.0.0 || ^8.0.0-0"
}
},
"node_modules/babel-preset-jest": {
@ -3359,9 +3359,9 @@
"dev": true
},
"node_modules/borp": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/borp/-/borp-0.20.0.tgz",
"integrity": "sha512-SZhSNosPoX6c9gtXKnakpkUYdAyQMkQDQwPzpPoIBV9ErWhWH2ks6paai27R37O/MNC9jLMW1lQojuZEOAF87g==",
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/borp/-/borp-0.20.1.tgz",
"integrity": "sha512-+1juusmbzAetePd0AIGbj+wut27bmzLFa2BhP8J9VGNcV7sx7bs4pDR2DhOU2oDZestWZpUzjfgIKYEI7LDW/A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3632,9 +3632,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001731",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
"integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"dev": true,
"funding": [
{
@ -4157,9 +4157,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
"version": "1.5.194",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz",
"integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==",
"dev": true,
"license": "ISC"
},
@ -4450,9 +4450,9 @@
}
},
"node_modules/eslint": {
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"version": "9.32.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4462,8 +4462,8 @@
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.15.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.31.0",
"@eslint/plugin-kit": "^0.3.1",
"@eslint/js": "9.32.0",
"@eslint/plugin-kit": "^0.3.4",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@ -4756,9 +4756,9 @@
}
},
"node_modules/eslint-plugin-n": {
"version": "17.21.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz",
"integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==",
"version": "17.21.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz",
"integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4767,8 +4767,8 @@
"eslint-plugin-es-x": "^7.8.0",
"get-tsconfig": "^4.8.1",
"globals": "^15.11.0",
"globrex": "^0.1.2",
"ignore": "^5.3.2",
"minimatch": "^9.0.5",
"semver": "^7.6.3",
"ts-declaration-location": "^1.0.6"
},
@ -4782,16 +4782,6 @@
"eslint": ">=8.23.0"
}
},
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/eslint-plugin-n/node_modules/globals": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
@ -4805,22 +4795,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-n/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/eslint-plugin-n/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -5640,6 +5614,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true,
"license": "MIT"
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -10066,16 +10047,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.37.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.37.0.tgz",
"integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==",
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz",
"integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/typescript-estree": "8.37.0",
"@typescript-eslint/utils": "8.37.0"
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/typescript-estree": "8.38.0",
"@typescript-eslint/utils": "8.38.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View file

@ -1,6 +1,6 @@
{
"name": "undici",
"version": "7.12.0",
"version": "7.13.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {

View file

@ -22,14 +22,10 @@ declare namespace Agent {
export interface Options extends Pool.Options {
/** Default: `(origin, opts) => new Pool(origin, opts)`. */
factory?(origin: string | URL, opts: Object): Dispatcher;
/** Integer. Default: `0` */
maxRedirections?: number;
interceptors?: { Agent?: readonly Dispatcher.DispatchInterceptor[] } & Pool.Options['interceptors']
}
export interface DispatchOptions extends Dispatcher.DispatchOptions {
/** Integer. */
maxRedirections?: number;
}
}

View file

@ -71,8 +71,6 @@ export declare namespace Client {
/** TODO */
maxCachedSessions?: number;
/** TODO */
maxRedirections?: number;
/** TODO */
connect?: Partial<buildConnector.BuildOptions> | buildConnector.connector;
/** TODO */
maxRequestsPerClient?: number;

View file

@ -135,8 +135,6 @@ declare namespace Dispatcher {
signal?: AbortSignal | EventEmitter | null;
/** This argument parameter is passed through to `ConnectData` */
opaque?: TOpaque;
/** Default: 0 */
maxRedirections?: number;
/** Default: false */
redirectionLimitReached?: boolean;
/** Default: `null` */
@ -147,8 +145,6 @@ declare namespace Dispatcher {
opaque?: TOpaque;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
/** Default: 0 */
maxRedirections?: number;
/** Default: false */
redirectionLimitReached?: boolean;
/** Default: `null` */
@ -172,8 +168,6 @@ declare namespace Dispatcher {
protocol?: string;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
/** Default: 0 */
maxRedirections?: number;
/** Default: false */
redirectionLimitReached?: boolean;
/** Default: `null` */

View file

@ -51,8 +51,6 @@ export declare namespace H2CClient {
/** TODO */
maxCachedSessions?: number;
/** TODO */
maxRedirections?: number;
/** TODO */
connect?: Omit<Partial<buildConnector.BuildOptions>, 'allowH2'> | buildConnector.connector;
/** TODO */
maxRequestsPerClient?: number;

View file

@ -13,6 +13,7 @@ import Agent from './agent'
import MockClient from './mock-client'
import MockPool from './mock-pool'
import MockAgent from './mock-agent'
import { SnapshotAgent } from './snapshot-agent'
import { MockCallHistory, MockCallHistoryLog } from './mock-call-history'
import mockErrors from './mock-errors'
import ProxyAgent from './proxy-agent'
@ -33,7 +34,7 @@ export * from './content-type'
export * from './cache'
export { Interceptable } from './mock-interceptor'
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient }
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, interceptors, MockClient, MockPool, MockAgent, SnapshotAgent, MockCallHistory, MockCallHistoryLog, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent, H2CClient }
export default Undici
declare namespace Undici {
@ -58,6 +59,7 @@ declare namespace Undici {
const MockClient: typeof import('./mock-client').default
const MockPool: typeof import('./mock-pool').default
const MockAgent: typeof import('./mock-agent').default
const SnapshotAgent: typeof import('./snapshot-agent').SnapshotAgent
const MockCallHistory: typeof import('./mock-call-history').MockCallHistory
const MockCallHistoryLog: typeof import('./mock-call-history').MockCallHistoryLog
const mockErrors: typeof import('./mock-errors').default

View file

@ -69,7 +69,6 @@ declare namespace MockInterceptor {
headers?: Headers | Record<string, string>;
origin?: string;
body?: BodyInit | Dispatcher.DispatchOptions['body'] | null;
maxRedirections?: number;
}
export type MockResponseDataHandler<TData extends object = object> = (

View file

@ -0,0 +1,107 @@
import MockAgent from './mock-agent'
declare class SnapshotRecorder {
constructor (options?: SnapshotRecorder.Options)
record (requestOpts: any, response: any): Promise<void>
findSnapshot (requestOpts: any): SnapshotRecorder.Snapshot | undefined
loadSnapshots (filePath?: string): Promise<void>
saveSnapshots (filePath?: string): Promise<void>
clear (): void
getSnapshots (): SnapshotRecorder.Snapshot[]
size (): number
resetCallCounts (): void
deleteSnapshot (requestOpts: any): boolean
getSnapshotInfo (requestOpts: any): SnapshotRecorder.SnapshotInfo | null
replaceSnapshots (snapshotData: SnapshotRecorder.SnapshotData[]): void
destroy (): void
}
declare namespace SnapshotRecorder {
export interface Options {
snapshotPath?: string
mode?: 'record' | 'playback' | 'update'
maxSnapshots?: number
autoFlush?: boolean
flushInterval?: number
matchHeaders?: string[]
ignoreHeaders?: string[]
excludeHeaders?: string[]
matchBody?: boolean
matchQuery?: boolean
caseSensitive?: boolean
shouldRecord?: (requestOpts: any) => boolean
shouldPlayback?: (requestOpts: any) => boolean
excludeUrls?: (string | RegExp)[]
}
export interface Snapshot {
request: {
method: string
url: string
headers: Record<string, string>
body?: string
}
responses: {
statusCode: number
headers: Record<string, string>
body: string
trailers: Record<string, string>
}[]
callCount: number
timestamp: string
}
export interface SnapshotInfo {
hash: string
request: {
method: string
url: string
headers: Record<string, string>
body?: string
}
responseCount: number
callCount: number
timestamp: string
}
export interface SnapshotData {
hash: string
snapshot: Snapshot
}
}
declare class SnapshotAgent extends MockAgent {
constructor (options?: SnapshotAgent.Options)
saveSnapshots (filePath?: string): Promise<void>
loadSnapshots (filePath?: string): Promise<void>
getRecorder (): SnapshotRecorder
getMode (): 'record' | 'playback' | 'update'
clearSnapshots (): void
resetCallCounts (): void
deleteSnapshot (requestOpts: any): boolean
getSnapshotInfo (requestOpts: any): SnapshotRecorder.SnapshotInfo | null
replaceSnapshots (snapshotData: SnapshotRecorder.SnapshotData[]): void
}
declare namespace SnapshotAgent {
export interface Options extends MockAgent.Options {
mode?: 'record' | 'playback' | 'update'
snapshotPath?: string
maxSnapshots?: number
autoFlush?: boolean
flushInterval?: number
matchHeaders?: string[]
ignoreHeaders?: string[]
excludeHeaders?: string[]
matchBody?: boolean
matchQuery?: boolean
caseSensitive?: boolean
shouldRecord?: (requestOpts: any) => boolean
shouldPlayback?: (requestOpts: any) => boolean
excludeUrls?: (string | RegExp)[]
}
}
export { SnapshotAgent, SnapshotRecorder }

149
deps/undici/undici.js vendored
View file

@ -1019,7 +1019,6 @@ var require_util = __commonJS({
var { IncomingMessage } = require("node:http");
var stream = require("node:stream");
var net = require("node:net");
var { Blob: Blob2 } = require("node:buffer");
var { stringify } = require("node:querystring");
var { EventEmitter: EE } = require("node:events");
var timers = require_timers();
@ -1074,7 +1073,7 @@ var require_util = __commonJS({
function isBlobLike(object) {
if (object === null) {
return false;
} else if (object instanceof Blob2) {
} else if (object instanceof Blob) {
return true;
} else if (typeof object !== "object") {
return false;
@ -4365,7 +4364,7 @@ var require_webidl = __commonJS({
webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream);
webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob);
webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams);
webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require("node:buffer").File);
webidl.is.File = webidl.util.MakeTypeAssertion(File);
webidl.is.URL = webidl.util.MakeTypeAssertion(URL);
webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal);
webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort);
@ -5472,9 +5471,7 @@ var require_formdata = __commonJS({
var { iteratorMixin } = require_util2();
var { kEnumerableProperty } = require_util();
var { webidl } = require_webidl();
var { File: NativeFile } = require("node:buffer");
var nodeUtil = require("node:util");
var File = globalThis.File ?? NativeFile;
var FormData = class _FormData {
static {
__name(this, "FormData");
@ -5643,8 +5640,6 @@ var require_formdata_parser = __commonJS({
var { makeEntry } = require_formdata();
var { webidl } = require_webidl();
var assert = require("node:assert");
var { File: NodeFile } = require("node:buffer");
var File = globalThis.File ?? NodeFile;
var formDataNameBuffer = Buffer.from('form-data; name="');
var filenameBuffer = Buffer.from("filename");
var dd = Buffer.from("--");
@ -5946,7 +5941,6 @@ var require_body = __commonJS({
} = require_util2();
var { FormData, setFormDataState } = require_formdata();
var { webidl } = require_webidl();
var { Blob: Blob2 } = require("node:buffer");
var assert = require("node:assert");
var { isErrored, isDisturbed } = require("node:stream");
var { isArrayBuffer } = require("node:util/types");
@ -6141,7 +6135,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType);
}
return new Blob2([bytes], { type: mimeType });
return new Blob([bytes], { type: mimeType });
}, instance, getInternalState);
},
arrayBuffer() {
@ -8787,7 +8781,7 @@ var require_global2 = __commonJS({
var require_proxy_agent = __commonJS({
"lib/dispatcher/proxy-agent.js"(exports2, module2) {
"use strict";
var { kProxy, kClose, kDestroy, kDispatch, kConnector } = require_symbols();
var { kProxy, kClose, kDestroy, kDispatch } = require_symbols();
var { URL: URL2 } = require("node:url");
var Agent = require_agent();
var Pool = require_pool();
@ -8812,56 +8806,59 @@ var require_proxy_agent = __commonJS({
__name(defaultFactory, "defaultFactory");
var noop = /* @__PURE__ */ __name(() => {
}, "noop");
var ProxyClient = class extends DispatcherBase {
function defaultAgentFactory(origin, opts) {
if (opts.connections === 1) {
return new Client(origin, opts);
}
return new Pool(origin, opts);
}
__name(defaultAgentFactory, "defaultAgentFactory");
var Http1ProxyWrapper = class extends DispatcherBase {
static {
__name(this, "ProxyClient");
}
#client = null;
constructor(origin, opts) {
if (typeof origin === "string") {
origin = new URL2(origin);
}
if (origin.protocol !== "http:" && origin.protocol !== "https:") {
throw new InvalidArgumentError("ProxyClient only supports http and https protocols");
__name(this, "Http1ProxyWrapper");
}
#client;
constructor(proxyUrl, { headers = {}, connect, factory }) {
super();
this.#client = new Client(origin, opts);
if (!proxyUrl) {
throw new InvalidArgumentError("Proxy URL is mandatory");
}
async [kClose]() {
await this.#client.close();
}
async [kDestroy]() {
await this.#client.destroy();
}
async [kDispatch](opts, handler) {
const { method, origin } = opts;
if (method === "CONNECT") {
this.#client[kConnector](
{
origin,
port: opts.port || defaultProtocolPort(opts.protocol),
path: opts.host,
signal: opts.signal,
headers: {
...this[kProxyHeaders],
host: opts.host
},
servername: this[kProxyTls]?.servername || opts.servername
},
(err, socket) => {
if (err) {
handler.callback(err);
this[kProxyHeaders] = headers;
if (factory) {
this.#client = factory(proxyUrl, { connect });
} else {
handler.callback(null, { socket, statusCode: 200 });
this.#client = new Client(proxyUrl, { connect });
}
}
);
[kDispatch](opts, handler) {
const onHeaders = handler.onHeaders;
handler.onHeaders = function(statusCode, data, resume) {
if (statusCode === 407) {
if (typeof handler.onError === "function") {
handler.onError(new InvalidArgumentError("Proxy Authentication Required (407)"));
}
return;
}
if (typeof origin === "string") {
opts.origin = new URL2(origin);
if (onHeaders) onHeaders.call(this, statusCode, data, resume);
};
const {
origin,
path = "/",
headers = {}
} = opts;
opts.path = origin + path;
if (!("host" in headers) && !("Host" in headers)) {
const { host } = new URL2(origin);
headers.host = host;
}
return this.#client.dispatch(opts, handler);
opts.headers = { ...this[kProxyHeaders], ...headers };
return this.#client[kDispatch](opts, handler);
}
async [kClose]() {
return this.#client.close();
}
async [kDestroy](err) {
return this.#client.destroy(err);
}
};
var ProxyAgent = class extends DispatcherBase {
@ -8884,6 +8881,7 @@ var require_proxy_agent = __commonJS({
this[kRequestTls] = opts.requestTls;
this[kProxyTls] = opts.proxyTls;
this[kProxyHeaders] = opts.headers || {};
this[kTunnelProxy] = proxyTunnel;
if (opts.auth && opts.token) {
throw new InvalidArgumentError("opts.auth cannot be used in combination with opts.token");
} else if (opts.auth) {
@ -8893,18 +8891,24 @@ var require_proxy_agent = __commonJS({
} else if (username && password) {
this[kProxyHeaders]["proxy-authorization"] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString("base64")}`;
}
const factory = !proxyTunnel && protocol === "http:" ? (origin2, options) => {
if (origin2.protocol === "http:") {
return new ProxyClient(origin2, options);
}
return new Client(origin2, options);
} : void 0;
const connect = buildConnector({ ...opts.proxyTls });
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls });
this[kClient] = clientFactory(url, { connect, factory });
this[kTunnelProxy] = proxyTunnel;
const agentFactory = opts.factory || defaultAgentFactory;
const factory = /* @__PURE__ */ __name((origin2, options) => {
const { protocol: protocol2 } = new URL2(origin2);
if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") {
return new Http1ProxyWrapper(this[kProxy].uri, {
headers: this[kProxyHeaders],
connect,
factory: agentFactory
});
}
return agentFactory(origin2, options);
}, "factory");
this[kClient] = clientFactory(url, { connect });
this[kAgent] = new Agent({
...opts,
factory,
connect: /* @__PURE__ */ __name(async (opts2, callback) => {
let requestedPath = opts2.host;
if (!opts2.port) {
@ -8955,9 +8959,6 @@ var require_proxy_agent = __commonJS({
const { host } = new URL2(opts.origin);
headers.host = host;
}
if (!this.#shouldConnect(new URL2(opts.origin))) {
opts.path = opts.origin + opts.path;
}
return this[kAgent].dispatch(
{
...opts,
@ -8987,18 +8988,6 @@ var require_proxy_agent = __commonJS({
await this[kAgent].destroy();
await this[kClient].destroy();
}
#shouldConnect(uri) {
if (typeof uri === "string") {
uri = new URL2(uri);
}
if (this[kTunnelProxy]) {
return true;
}
if (uri.protocol !== "http:" || this[kProxy].protocol !== "http:") {
return true;
}
return false;
}
};
function buildHeaders(headers) {
if (Array.isArray(headers)) {
@ -14273,7 +14262,7 @@ var require_readable = __commonJS({
}
}
/**
* @param {string} event
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
@ -14285,7 +14274,7 @@ var require_readable = __commonJS({
return super.on(event, listener);
}
/**
* @param {string} event
* @param {string|symbol} event
* @param {(...args: any[]) => void} listener
* @returns {this}
*/
@ -14317,11 +14306,13 @@ var require_readable = __commonJS({
* @returns {boolean}
*/
push(chunk) {
this[kBytesRead] += chunk ? chunk.length : 0;
if (this[kConsume] && chunk !== null) {
if (chunk) {
this[kBytesRead] += chunk.length;
if (this[kConsume]) {
consumePush(this[kConsume], chunk);
return this[kReading] ? super.push(chunk) : true;
}
}
return super.push(chunk);
}
/**
@ -14473,9 +14464,7 @@ var require_readable = __commonJS({
if (isUnusable(stream)) {
const rState = stream._readableState;
if (rState.destroyed && rState.closeEmitted === false) {
stream.on("error", (err) => {
reject(err);
}).on("close", () => {
stream.on("error", reject).on("close", () => {
reject(new TypeError("unusable"));
});
} else {

View file

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