undici 7.12.0 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,517 @@
1
+ 'use strict'
2
+
3
+ const { writeFile, readFile, mkdir } = require('node:fs/promises')
4
+ const { dirname, resolve } = require('node:path')
5
+ const { InvalidArgumentError, UndiciError } = require('../core/errors')
6
+
7
+ /**
8
+ * Formats a request for consistent snapshot storage
9
+ * Caches normalized headers to avoid repeated processing
10
+ */
11
+ function formatRequestKey (opts, cachedSets, matchOptions = {}) {
12
+ const url = new URL(opts.path, opts.origin)
13
+
14
+ // Cache normalized headers if not already done
15
+ const normalized = opts._normalizedHeaders || normalizeHeaders(opts.headers)
16
+ if (!opts._normalizedHeaders) {
17
+ opts._normalizedHeaders = normalized
18
+ }
19
+
20
+ return {
21
+ method: opts.method || 'GET',
22
+ url: matchOptions.matchQuery !== false ? url.toString() : `${url.origin}${url.pathname}`,
23
+ headers: filterHeadersForMatching(normalized, cachedSets, matchOptions),
24
+ body: matchOptions.matchBody !== false && opts.body ? String(opts.body) : undefined
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Filters headers based on matching configuration
30
+ */
31
+ function filterHeadersForMatching (headers, cachedSets, matchOptions = {}) {
32
+ if (!headers || typeof headers !== 'object') return {}
33
+
34
+ const {
35
+ matchHeaders = null,
36
+ caseSensitive = false
37
+ } = matchOptions
38
+
39
+ const filtered = {}
40
+ const { ignoreSet, excludeSet, matchSet } = cachedSets
41
+
42
+ for (const [key, value] of Object.entries(headers)) {
43
+ const headerKey = caseSensitive ? key : key.toLowerCase()
44
+
45
+ // Skip if in exclude list (for security)
46
+ if (excludeSet.has(headerKey)) continue
47
+
48
+ // Skip if in ignore list (for matching)
49
+ if (ignoreSet.has(headerKey)) continue
50
+
51
+ // If matchHeaders is specified, only include those headers
52
+ if (matchHeaders && Array.isArray(matchHeaders)) {
53
+ if (!matchSet.has(headerKey)) continue
54
+ }
55
+
56
+ filtered[headerKey] = value
57
+ }
58
+
59
+ return filtered
60
+ }
61
+
62
+ /**
63
+ * Filters headers for storage (only excludes sensitive headers)
64
+ */
65
+ function filterHeadersForStorage (headers, matchOptions = {}) {
66
+ if (!headers || typeof headers !== 'object') return {}
67
+
68
+ const {
69
+ excludeHeaders = [],
70
+ caseSensitive = false
71
+ } = matchOptions
72
+
73
+ const filtered = {}
74
+ const excludeSet = new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
75
+
76
+ for (const [key, value] of Object.entries(headers)) {
77
+ const headerKey = caseSensitive ? key : key.toLowerCase()
78
+
79
+ // Skip if in exclude list (for security)
80
+ if (excludeSet.has(headerKey)) continue
81
+
82
+ filtered[headerKey] = value
83
+ }
84
+
85
+ return filtered
86
+ }
87
+
88
+ /**
89
+ * Creates cached header sets for performance
90
+ */
91
+ function createHeaderSetsCache (matchOptions = {}) {
92
+ const { ignoreHeaders = [], excludeHeaders = [], matchHeaders = null, caseSensitive = false } = matchOptions
93
+
94
+ return {
95
+ ignoreSet: new Set(ignoreHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
96
+ excludeSet: new Set(excludeHeaders.map(h => caseSensitive ? h : h.toLowerCase())),
97
+ matchSet: matchHeaders && Array.isArray(matchHeaders)
98
+ ? new Set(matchHeaders.map(h => caseSensitive ? h : h.toLowerCase()))
99
+ : null
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Normalizes headers for consistent comparison
105
+ */
106
+ function normalizeHeaders (headers) {
107
+ if (!headers) return {}
108
+
109
+ const normalized = {}
110
+
111
+ // Handle array format (undici internal format: [name, value, name, value, ...])
112
+ if (Array.isArray(headers)) {
113
+ for (let i = 0; i < headers.length; i += 2) {
114
+ const key = headers[i]
115
+ const value = headers[i + 1]
116
+ if (key && value !== undefined) {
117
+ // Convert Buffers to strings if needed
118
+ const keyStr = Buffer.isBuffer(key) ? key.toString() : String(key)
119
+ const valueStr = Buffer.isBuffer(value) ? value.toString() : String(value)
120
+ normalized[keyStr.toLowerCase()] = valueStr
121
+ }
122
+ }
123
+ return normalized
124
+ }
125
+
126
+ // Handle object format
127
+ if (headers && typeof headers === 'object') {
128
+ for (const [key, value] of Object.entries(headers)) {
129
+ if (key && typeof key === 'string') {
130
+ normalized[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value)
131
+ }
132
+ }
133
+ }
134
+
135
+ return normalized
136
+ }
137
+
138
+ /**
139
+ * Creates a hash key for request matching
140
+ */
141
+ function createRequestHash (request) {
142
+ const parts = [
143
+ request.method,
144
+ request.url,
145
+ JSON.stringify(request.headers, Object.keys(request.headers).sort()),
146
+ request.body || ''
147
+ ]
148
+ return Buffer.from(parts.join('|')).toString('base64url')
149
+ }
150
+
151
+ /**
152
+ * Checks if a URL matches any of the exclude patterns
153
+ */
154
+ function isUrlExcluded (url, excludePatterns = []) {
155
+ if (!excludePatterns.length) return false
156
+
157
+ for (const pattern of excludePatterns) {
158
+ if (typeof pattern === 'string') {
159
+ // Simple string match (case-insensitive)
160
+ if (url.toLowerCase().includes(pattern.toLowerCase())) {
161
+ return true
162
+ }
163
+ } else if (pattern instanceof RegExp) {
164
+ // Regex pattern match
165
+ if (pattern.test(url)) {
166
+ return true
167
+ }
168
+ }
169
+ }
170
+
171
+ return false
172
+ }
173
+
174
+ class SnapshotRecorder {
175
+ constructor (options = {}) {
176
+ this.snapshots = new Map()
177
+ this.snapshotPath = options.snapshotPath
178
+ this.mode = options.mode || 'record'
179
+ this.loaded = false
180
+ this.maxSnapshots = options.maxSnapshots || Infinity
181
+ this.autoFlush = options.autoFlush || false
182
+ this.flushInterval = options.flushInterval || 30000 // 30 seconds default
183
+ this._flushTimer = null
184
+ this._flushTimeout = null
185
+
186
+ // Matching configuration
187
+ this.matchOptions = {
188
+ matchHeaders: options.matchHeaders || null, // null means match all headers
189
+ ignoreHeaders: options.ignoreHeaders || [],
190
+ excludeHeaders: options.excludeHeaders || [],
191
+ matchBody: options.matchBody !== false, // default: true
192
+ matchQuery: options.matchQuery !== false, // default: true
193
+ caseSensitive: options.caseSensitive || false
194
+ }
195
+
196
+ // Cache processed header sets to avoid recreating them on every request
197
+ this._headerSetsCache = createHeaderSetsCache(this.matchOptions)
198
+
199
+ // Request filtering callbacks
200
+ this.shouldRecord = options.shouldRecord || null // function(requestOpts) -> boolean
201
+ this.shouldPlayback = options.shouldPlayback || null // function(requestOpts) -> boolean
202
+
203
+ // URL pattern filtering
204
+ this.excludeUrls = options.excludeUrls || [] // Array of regex patterns or strings
205
+
206
+ // Start auto-flush timer if enabled
207
+ if (this.autoFlush && this.snapshotPath) {
208
+ this._startAutoFlush()
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Records a request-response interaction
214
+ */
215
+ async record (requestOpts, response) {
216
+ // Check if recording should be filtered out
217
+ if (this.shouldRecord && typeof this.shouldRecord === 'function') {
218
+ if (!this.shouldRecord(requestOpts)) {
219
+ return // Skip recording
220
+ }
221
+ }
222
+
223
+ // Check URL exclusion patterns
224
+ const url = new URL(requestOpts.path, requestOpts.origin).toString()
225
+ if (isUrlExcluded(url, this.excludeUrls)) {
226
+ return // Skip recording
227
+ }
228
+
229
+ const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
230
+ const hash = createRequestHash(request)
231
+
232
+ // Extract response data - always store body as base64
233
+ const normalizedHeaders = normalizeHeaders(response.headers)
234
+ const responseData = {
235
+ statusCode: response.statusCode,
236
+ headers: filterHeadersForStorage(normalizedHeaders, this.matchOptions),
237
+ body: Buffer.isBuffer(response.body)
238
+ ? response.body.toString('base64')
239
+ : Buffer.from(String(response.body || '')).toString('base64'),
240
+ trailers: response.trailers
241
+ }
242
+
243
+ // Remove oldest snapshot if we exceed maxSnapshots limit
244
+ if (this.snapshots.size >= this.maxSnapshots && !this.snapshots.has(hash)) {
245
+ const oldestKey = this.snapshots.keys().next().value
246
+ this.snapshots.delete(oldestKey)
247
+ }
248
+
249
+ // Support sequential responses - if snapshot exists, add to responses array
250
+ const existingSnapshot = this.snapshots.get(hash)
251
+ if (existingSnapshot && existingSnapshot.responses) {
252
+ existingSnapshot.responses.push(responseData)
253
+ existingSnapshot.timestamp = new Date().toISOString()
254
+ } else {
255
+ this.snapshots.set(hash, {
256
+ request,
257
+ responses: [responseData], // Always store as array for consistency
258
+ callCount: 0,
259
+ timestamp: new Date().toISOString()
260
+ })
261
+ }
262
+
263
+ // Auto-flush if enabled
264
+ if (this.autoFlush && this.snapshotPath) {
265
+ this._scheduleFlush()
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Finds a matching snapshot for the given request
271
+ * Returns the appropriate response based on call count for sequential responses
272
+ */
273
+ findSnapshot (requestOpts) {
274
+ // Check if playback should be filtered out
275
+ if (this.shouldPlayback && typeof this.shouldPlayback === 'function') {
276
+ if (!this.shouldPlayback(requestOpts)) {
277
+ return undefined // Skip playback
278
+ }
279
+ }
280
+
281
+ // Check URL exclusion patterns
282
+ const url = new URL(requestOpts.path, requestOpts.origin).toString()
283
+ if (isUrlExcluded(url, this.excludeUrls)) {
284
+ return undefined // Skip playback
285
+ }
286
+
287
+ const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
288
+ const hash = createRequestHash(request)
289
+ const snapshot = this.snapshots.get(hash)
290
+
291
+ if (!snapshot) return undefined
292
+
293
+ // Handle sequential responses
294
+ if (snapshot.responses && Array.isArray(snapshot.responses)) {
295
+ const currentCallCount = snapshot.callCount || 0
296
+ const responseIndex = Math.min(currentCallCount, snapshot.responses.length - 1)
297
+ snapshot.callCount = currentCallCount + 1
298
+
299
+ return {
300
+ ...snapshot,
301
+ response: snapshot.responses[responseIndex]
302
+ }
303
+ }
304
+
305
+ // Legacy format compatibility - convert single response to array format
306
+ if (snapshot.response && !snapshot.responses) {
307
+ snapshot.responses = [snapshot.response]
308
+ snapshot.callCount = 1
309
+ delete snapshot.response
310
+
311
+ return {
312
+ ...snapshot,
313
+ response: snapshot.responses[0]
314
+ }
315
+ }
316
+
317
+ return snapshot
318
+ }
319
+
320
+ /**
321
+ * Loads snapshots from file
322
+ */
323
+ async loadSnapshots (filePath) {
324
+ const path = filePath || this.snapshotPath
325
+ if (!path) {
326
+ throw new InvalidArgumentError('Snapshot path is required')
327
+ }
328
+
329
+ try {
330
+ const data = await readFile(resolve(path), 'utf8')
331
+ const parsed = JSON.parse(data)
332
+
333
+ // Convert array format back to Map
334
+ if (Array.isArray(parsed)) {
335
+ this.snapshots.clear()
336
+ for (const { hash, snapshot } of parsed) {
337
+ this.snapshots.set(hash, snapshot)
338
+ }
339
+ } else {
340
+ // Legacy object format
341
+ this.snapshots = new Map(Object.entries(parsed))
342
+ }
343
+
344
+ this.loaded = true
345
+ } catch (error) {
346
+ if (error.code === 'ENOENT') {
347
+ // File doesn't exist yet - that's ok for recording mode
348
+ this.snapshots.clear()
349
+ this.loaded = true
350
+ } else {
351
+ throw new UndiciError(`Failed to load snapshots from ${path}`, { cause: error })
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Saves snapshots to file
358
+ */
359
+ async saveSnapshots (filePath) {
360
+ const path = filePath || this.snapshotPath
361
+ if (!path) {
362
+ throw new InvalidArgumentError('Snapshot path is required')
363
+ }
364
+
365
+ const resolvedPath = resolve(path)
366
+
367
+ // Ensure directory exists
368
+ await mkdir(dirname(resolvedPath), { recursive: true })
369
+
370
+ // Convert Map to serializable format
371
+ const data = Array.from(this.snapshots.entries()).map(([hash, snapshot]) => ({
372
+ hash,
373
+ snapshot
374
+ }))
375
+
376
+ await writeFile(resolvedPath, JSON.stringify(data, null, 2), 'utf8', { flush: true })
377
+ }
378
+
379
+ /**
380
+ * Clears all recorded snapshots
381
+ */
382
+ clear () {
383
+ this.snapshots.clear()
384
+ }
385
+
386
+ /**
387
+ * Gets all recorded snapshots
388
+ */
389
+ getSnapshots () {
390
+ return Array.from(this.snapshots.values())
391
+ }
392
+
393
+ /**
394
+ * Gets snapshot count
395
+ */
396
+ size () {
397
+ return this.snapshots.size
398
+ }
399
+
400
+ /**
401
+ * Resets call counts for all snapshots (useful for test cleanup)
402
+ */
403
+ resetCallCounts () {
404
+ for (const snapshot of this.snapshots.values()) {
405
+ snapshot.callCount = 0
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Deletes a specific snapshot by request options
411
+ */
412
+ deleteSnapshot (requestOpts) {
413
+ const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
414
+ const hash = createRequestHash(request)
415
+ return this.snapshots.delete(hash)
416
+ }
417
+
418
+ /**
419
+ * Gets information about a specific snapshot
420
+ */
421
+ getSnapshotInfo (requestOpts) {
422
+ const request = formatRequestKey(requestOpts, this._headerSetsCache, this.matchOptions)
423
+ const hash = createRequestHash(request)
424
+ const snapshot = this.snapshots.get(hash)
425
+
426
+ if (!snapshot) return null
427
+
428
+ return {
429
+ hash,
430
+ request: snapshot.request,
431
+ responseCount: snapshot.responses ? snapshot.responses.length : (snapshot.response ? 1 : 0),
432
+ callCount: snapshot.callCount || 0,
433
+ timestamp: snapshot.timestamp
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Replaces all snapshots with new data (full replacement)
439
+ */
440
+ replaceSnapshots (snapshotData) {
441
+ this.snapshots.clear()
442
+
443
+ if (Array.isArray(snapshotData)) {
444
+ for (const { hash, snapshot } of snapshotData) {
445
+ this.snapshots.set(hash, snapshot)
446
+ }
447
+ } else if (snapshotData && typeof snapshotData === 'object') {
448
+ // Legacy object format
449
+ this.snapshots = new Map(Object.entries(snapshotData))
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Starts the auto-flush timer
455
+ */
456
+ _startAutoFlush () {
457
+ if (!this._flushTimer) {
458
+ this._flushTimer = setInterval(() => {
459
+ this.saveSnapshots().catch(() => {
460
+ // Ignore flush errors - they shouldn't interrupt normal operation
461
+ })
462
+ }, this.flushInterval)
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Stops the auto-flush timer
468
+ */
469
+ _stopAutoFlush () {
470
+ if (this._flushTimer) {
471
+ clearInterval(this._flushTimer)
472
+ this._flushTimer = null
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Schedules a flush (debounced to avoid excessive writes)
478
+ */
479
+ _scheduleFlush () {
480
+ // Simple debouncing - clear existing timeout and set new one
481
+ if (this._flushTimeout) {
482
+ clearTimeout(this._flushTimeout)
483
+ }
484
+ this._flushTimeout = setTimeout(() => {
485
+ this.saveSnapshots().catch(() => {
486
+ // Ignore flush errors
487
+ })
488
+ this._flushTimeout = null
489
+ }, 1000) // 1 second debounce
490
+ }
491
+
492
+ /**
493
+ * Cleanup method to stop timers
494
+ */
495
+ destroy () {
496
+ this._stopAutoFlush()
497
+ if (this._flushTimeout) {
498
+ clearTimeout(this._flushTimeout)
499
+ this._flushTimeout = null
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Async close method that saves all recordings and performs cleanup
505
+ */
506
+ async close () {
507
+ // Save any pending recordings if we have a snapshot path
508
+ if (this.snapshotPath && this.snapshots.size > 0) {
509
+ await this.saveSnapshots()
510
+ }
511
+
512
+ // Perform cleanup
513
+ this.destroy()
514
+ }
515
+ }
516
+
517
+ module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filterHeadersForMatching, filterHeadersForStorage, isUrlExcluded, createHeaderSetsCache }
@@ -10,7 +10,6 @@ const {
10
10
  } = require('./util')
11
11
  const { FormData, setFormDataState } = require('./formdata')
12
12
  const { webidl } = require('../webidl')
13
- const { Blob } = require('node:buffer')
14
13
  const assert = require('node:assert')
15
14
  const { isErrored, isDisturbed } = require('node:stream')
16
15
  const { isArrayBuffer } = require('node:util/types')
@@ -6,9 +6,6 @@ const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
6
6
  const { makeEntry } = require('./formdata')
7
7
  const { webidl } = require('../webidl')
8
8
  const assert = require('node:assert')
9
- const { File: NodeFile } = require('node:buffer')
10
-
11
- const File = globalThis.File ?? NodeFile
12
9
 
13
10
  const formDataNameBuffer = Buffer.from('form-data; name="')
14
11
  const filenameBuffer = Buffer.from('filename')
@@ -3,12 +3,8 @@
3
3
  const { iteratorMixin } = require('./util')
4
4
  const { kEnumerableProperty } = require('../../core/util')
5
5
  const { webidl } = require('../webidl')
6
- const { File: NativeFile } = require('node:buffer')
7
6
  const nodeUtil = require('node:util')
8
7
 
9
- /** @type {globalThis['File']} */
10
- const File = globalThis.File ?? NativeFile
11
-
12
8
  // https://xhr.spec.whatwg.org/#formdata
13
9
  class FormData {
14
10
  #state = []
@@ -509,7 +509,7 @@ webidl.is.USVString = function (value) {
509
509
  webidl.is.ReadableStream = webidl.util.MakeTypeAssertion(ReadableStream)
510
510
  webidl.is.Blob = webidl.util.MakeTypeAssertion(Blob)
511
511
  webidl.is.URLSearchParams = webidl.util.MakeTypeAssertion(URLSearchParams)
512
- webidl.is.File = webidl.util.MakeTypeAssertion(globalThis.File ?? require('node:buffer').File)
512
+ webidl.is.File = webidl.util.MakeTypeAssertion(File)
513
513
  webidl.is.URL = webidl.util.MakeTypeAssertion(URL)
514
514
  webidl.is.AbortSignal = webidl.util.MakeTypeAssertion(AbortSignal)
515
515
  webidl.is.MessagePort = webidl.util.MakeTypeAssertion(MessagePort)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "7.12.0",
3
+ "version": "7.13.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
package/types/agent.d.ts CHANGED
@@ -22,14 +22,10 @@ declare namespace Agent {
22
22
  export interface Options extends Pool.Options {
23
23
  /** Default: `(origin, opts) => new Pool(origin, opts)`. */
24
24
  factory?(origin: string | URL, opts: Object): Dispatcher;
25
- /** Integer. Default: `0` */
26
- maxRedirections?: number;
27
25
 
28
26
  interceptors?: { Agent?: readonly Dispatcher.DispatchInterceptor[] } & Pool.Options['interceptors']
29
27
  }
30
28
 
31
29
  export interface DispatchOptions extends Dispatcher.DispatchOptions {
32
- /** Integer. */
33
- maxRedirections?: number;
34
30
  }
35
31
  }
package/types/client.d.ts CHANGED
@@ -71,8 +71,6 @@ export declare namespace Client {
71
71
  /** TODO */
72
72
  maxCachedSessions?: number;
73
73
  /** TODO */
74
- maxRedirections?: number;
75
- /** TODO */
76
74
  connect?: Partial<buildConnector.BuildOptions> | buildConnector.connector;
77
75
  /** TODO */
78
76
  maxRequestsPerClient?: number;
@@ -135,8 +135,6 @@ declare namespace Dispatcher {
135
135
  signal?: AbortSignal | EventEmitter | null;
136
136
  /** This argument parameter is passed through to `ConnectData` */
137
137
  opaque?: TOpaque;
138
- /** Default: 0 */
139
- maxRedirections?: number;
140
138
  /** Default: false */
141
139
  redirectionLimitReached?: boolean;
142
140
  /** Default: `null` */
@@ -147,8 +145,6 @@ declare namespace Dispatcher {
147
145
  opaque?: TOpaque;
148
146
  /** Default: `null` */
149
147
  signal?: AbortSignal | EventEmitter | null;
150
- /** Default: 0 */
151
- maxRedirections?: number;
152
148
  /** Default: false */
153
149
  redirectionLimitReached?: boolean;
154
150
  /** Default: `null` */
@@ -172,8 +168,6 @@ declare namespace Dispatcher {
172
168
  protocol?: string;
173
169
  /** Default: `null` */
174
170
  signal?: AbortSignal | EventEmitter | null;
175
- /** Default: 0 */
176
- maxRedirections?: number;
177
171
  /** Default: false */
178
172
  redirectionLimitReached?: boolean;
179
173
  /** Default: `null` */
@@ -51,8 +51,6 @@ export declare namespace H2CClient {
51
51
  /** TODO */
52
52
  maxCachedSessions?: number;
53
53
  /** TODO */
54
- maxRedirections?: number;
55
- /** TODO */
56
54
  connect?: Omit<Partial<buildConnector.BuildOptions>, 'allowH2'> | buildConnector.connector;
57
55
  /** TODO */
58
56
  maxRequestsPerClient?: number;
package/types/index.d.ts CHANGED
@@ -13,6 +13,7 @@ import Agent from './agent'
13
13
  import MockClient from './mock-client'
14
14
  import MockPool from './mock-pool'
15
15
  import MockAgent from './mock-agent'
16
+ import { SnapshotAgent } from './snapshot-agent'
16
17
  import { MockCallHistory, MockCallHistoryLog } from './mock-call-history'
17
18
  import mockErrors from './mock-errors'
18
19
  import ProxyAgent from './proxy-agent'
@@ -33,7 +34,7 @@ export * from './content-type'
33
34
  export * from './cache'
34
35
  export { Interceptable } from './mock-interceptor'
35
36
 
36
- 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 }
37
+ 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 }
37
38
  export default Undici
38
39
 
39
40
  declare namespace Undici {
@@ -58,6 +59,7 @@ declare namespace Undici {
58
59
  const MockClient: typeof import('./mock-client').default
59
60
  const MockPool: typeof import('./mock-pool').default
60
61
  const MockAgent: typeof import('./mock-agent').default
62
+ const SnapshotAgent: typeof import('./snapshot-agent').SnapshotAgent
61
63
  const MockCallHistory: typeof import('./mock-call-history').MockCallHistory
62
64
  const MockCallHistoryLog: typeof import('./mock-call-history').MockCallHistoryLog
63
65
  const mockErrors: typeof import('./mock-errors').default
@@ -69,7 +69,6 @@ declare namespace MockInterceptor {
69
69
  headers?: Headers | Record<string, string>;
70
70
  origin?: string;
71
71
  body?: BodyInit | Dispatcher.DispatchOptions['body'] | null;
72
- maxRedirections?: number;
73
72
  }
74
73
 
75
74
  export type MockResponseDataHandler<TData extends object = object> = (