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.
- package/README.md +11 -9
- package/docs/docs/api/ProxyAgent.md +1 -1
- package/docs/docs/api/SnapshotAgent.md +616 -0
- package/index.js +2 -0
- package/lib/api/readable.js +48 -26
- package/lib/core/util.js +0 -1
- package/lib/dispatcher/proxy-agent.js +67 -71
- package/lib/handler/redirect-handler.js +10 -0
- package/lib/interceptor/dump.js +2 -1
- package/lib/mock/mock-agent.js +10 -4
- package/lib/mock/snapshot-agent.js +333 -0
- package/lib/mock/snapshot-recorder.js +517 -0
- package/lib/web/fetch/body.js +0 -1
- package/lib/web/fetch/formdata-parser.js +0 -3
- package/lib/web/fetch/formdata.js +0 -4
- package/lib/web/webidl/index.js +1 -1
- package/package.json +1 -1
- package/types/agent.d.ts +0 -4
- package/types/client.d.ts +0 -2
- package/types/dispatcher.d.ts +0 -6
- package/types/h2c-client.d.ts +0 -2
- package/types/index.d.ts +3 -1
- package/types/mock-interceptor.d.ts +0 -1
- package/types/snapshot-agent.d.ts +107 -0
package/lib/api/readable.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Ported from https://github.com/nodejs/undici/pull/907
|
|
2
|
-
|
|
3
1
|
'use strict'
|
|
4
2
|
|
|
5
3
|
const assert = require('node:assert')
|
|
@@ -50,23 +48,32 @@ class BodyReadable extends Readable {
|
|
|
50
48
|
|
|
51
49
|
this[kAbort] = abort
|
|
52
50
|
|
|
53
|
-
/**
|
|
54
|
-
* @type {Consume | null}
|
|
55
|
-
*/
|
|
51
|
+
/** @type {Consume | null} */
|
|
56
52
|
this[kConsume] = null
|
|
53
|
+
|
|
54
|
+
/** @type {number} */
|
|
57
55
|
this[kBytesRead] = 0
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*/
|
|
56
|
+
|
|
57
|
+
/** @type {ReadableStream|null} */
|
|
61
58
|
this[kBody] = null
|
|
59
|
+
|
|
60
|
+
/** @type {boolean} */
|
|
62
61
|
this[kUsed] = false
|
|
62
|
+
|
|
63
|
+
/** @type {string} */
|
|
63
64
|
this[kContentType] = contentType
|
|
65
|
+
|
|
66
|
+
/** @type {number|null} */
|
|
64
67
|
this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Is stream being consumed through Readable API?
|
|
71
|
+
* This is an optimization so that we avoid checking
|
|
72
|
+
* for 'data' and 'readable' listeners in the hot path
|
|
73
|
+
* inside push().
|
|
74
|
+
*
|
|
75
|
+
* @type {boolean}
|
|
76
|
+
*/
|
|
70
77
|
this[kReading] = false
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -96,7 +103,7 @@ class BodyReadable extends Readable {
|
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/**
|
|
99
|
-
* @param {string} event
|
|
106
|
+
* @param {string|symbol} event
|
|
100
107
|
* @param {(...args: any[]) => void} listener
|
|
101
108
|
* @returns {this}
|
|
102
109
|
*/
|
|
@@ -109,7 +116,7 @@ class BodyReadable extends Readable {
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
|
-
* @param {string} event
|
|
119
|
+
* @param {string|symbol} event
|
|
113
120
|
* @param {(...args: any[]) => void} listener
|
|
114
121
|
* @returns {this}
|
|
115
122
|
*/
|
|
@@ -147,12 +154,14 @@ class BodyReadable extends Readable {
|
|
|
147
154
|
* @returns {boolean}
|
|
148
155
|
*/
|
|
149
156
|
push (chunk) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
if (chunk) {
|
|
158
|
+
this[kBytesRead] += chunk.length
|
|
159
|
+
if (this[kConsume]) {
|
|
160
|
+
consumePush(this[kConsume], chunk)
|
|
161
|
+
return this[kReading] ? super.push(chunk) : true
|
|
162
|
+
}
|
|
155
163
|
}
|
|
164
|
+
|
|
156
165
|
return super.push(chunk)
|
|
157
166
|
}
|
|
158
167
|
|
|
@@ -338,9 +347,23 @@ function isUnusable (bodyReadable) {
|
|
|
338
347
|
return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
/**
|
|
351
|
+
* @typedef {'text' | 'json' | 'blob' | 'bytes' | 'arrayBuffer'} ConsumeType
|
|
352
|
+
*/
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @template {ConsumeType} T
|
|
356
|
+
* @typedef {T extends 'text' ? string :
|
|
357
|
+
* T extends 'json' ? unknown :
|
|
358
|
+
* T extends 'blob' ? Blob :
|
|
359
|
+
* T extends 'arrayBuffer' ? ArrayBuffer :
|
|
360
|
+
* T extends 'bytes' ? Uint8Array :
|
|
361
|
+
* never
|
|
362
|
+
* } ConsumeReturnType
|
|
363
|
+
*/
|
|
341
364
|
/**
|
|
342
365
|
* @typedef {object} Consume
|
|
343
|
-
* @property {
|
|
366
|
+
* @property {ConsumeType} type
|
|
344
367
|
* @property {BodyReadable} stream
|
|
345
368
|
* @property {((value?: any) => void)} resolve
|
|
346
369
|
* @property {((err: Error) => void)} reject
|
|
@@ -349,9 +372,10 @@ function isUnusable (bodyReadable) {
|
|
|
349
372
|
*/
|
|
350
373
|
|
|
351
374
|
/**
|
|
375
|
+
* @template {ConsumeType} T
|
|
352
376
|
* @param {BodyReadable} stream
|
|
353
|
-
* @param {
|
|
354
|
-
* @returns {Promise<
|
|
377
|
+
* @param {T} type
|
|
378
|
+
* @returns {Promise<ConsumeReturnType<T>>}
|
|
355
379
|
*/
|
|
356
380
|
function consume (stream, type) {
|
|
357
381
|
assert(!stream[kConsume])
|
|
@@ -361,9 +385,7 @@ function consume (stream, type) {
|
|
|
361
385
|
const rState = stream._readableState
|
|
362
386
|
if (rState.destroyed && rState.closeEmitted === false) {
|
|
363
387
|
stream
|
|
364
|
-
.on('error',
|
|
365
|
-
reject(err)
|
|
366
|
-
})
|
|
388
|
+
.on('error', reject)
|
|
367
389
|
.on('close', () => {
|
|
368
390
|
reject(new TypeError('unusable'))
|
|
369
391
|
})
|
|
@@ -438,7 +460,7 @@ function consumeStart (consume) {
|
|
|
438
460
|
/**
|
|
439
461
|
* @param {Buffer[]} chunks
|
|
440
462
|
* @param {number} length
|
|
441
|
-
* @param {BufferEncoding} encoding
|
|
463
|
+
* @param {BufferEncoding} [encoding='utf8']
|
|
442
464
|
* @returns {string}
|
|
443
465
|
*/
|
|
444
466
|
function chunksDecode (chunks, length, encoding) {
|
package/lib/core/util.js
CHANGED
|
@@ -5,7 +5,6 @@ const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
|
|
|
5
5
|
const { IncomingMessage } = require('node:http')
|
|
6
6
|
const stream = require('node:stream')
|
|
7
7
|
const net = require('node:net')
|
|
8
|
-
const { Blob } = require('node:buffer')
|
|
9
8
|
const { stringify } = require('node:querystring')
|
|
10
9
|
const { EventEmitter: EE } = require('node:events')
|
|
11
10
|
const timers = require('../util/timers')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { kProxy, kClose, kDestroy, kDispatch
|
|
3
|
+
const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
|
4
4
|
const { URL } = require('node:url')
|
|
5
5
|
const Agent = require('./agent')
|
|
6
6
|
const Pool = require('./pool')
|
|
@@ -27,61 +27,69 @@ function defaultFactory (origin, opts) {
|
|
|
27
27
|
|
|
28
28
|
const noop = () => {}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
function defaultAgentFactory (origin, opts) {
|
|
31
|
+
if (opts.connections === 1) {
|
|
32
|
+
return new Client(origin, opts)
|
|
33
|
+
}
|
|
34
|
+
return new Pool(origin, opts)
|
|
35
|
+
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
37
|
+
class Http1ProxyWrapper extends DispatcherBase {
|
|
38
|
+
#client
|
|
40
39
|
|
|
40
|
+
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
|
41
41
|
super()
|
|
42
|
+
if (!proxyUrl) {
|
|
43
|
+
throw new InvalidArgumentError('Proxy URL is mandatory')
|
|
44
|
+
}
|
|
42
45
|
|
|
43
|
-
this
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
async [kDestroy] () {
|
|
51
|
-
await this.#client.destroy()
|
|
46
|
+
this[kProxyHeaders] = headers
|
|
47
|
+
if (factory) {
|
|
48
|
+
this.#client = factory(proxyUrl, { connect })
|
|
49
|
+
} else {
|
|
50
|
+
this.#client = new Client(proxyUrl, { connect })
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
path: opts.host,
|
|
61
|
-
signal: opts.signal,
|
|
62
|
-
headers: {
|
|
63
|
-
...this[kProxyHeaders],
|
|
64
|
-
host: opts.host
|
|
65
|
-
},
|
|
66
|
-
servername: this[kProxyTls]?.servername || opts.servername
|
|
67
|
-
},
|
|
68
|
-
(err, socket) => {
|
|
69
|
-
if (err) {
|
|
70
|
-
handler.callback(err)
|
|
71
|
-
} else {
|
|
72
|
-
handler.callback(null, { socket, statusCode: 200 })
|
|
54
|
+
[kDispatch] (opts, handler) {
|
|
55
|
+
const onHeaders = handler.onHeaders
|
|
56
|
+
handler.onHeaders = function (statusCode, data, resume) {
|
|
57
|
+
if (statusCode === 407) {
|
|
58
|
+
if (typeof handler.onError === 'function') {
|
|
59
|
+
handler.onError(new InvalidArgumentError('Proxy Authentication Required (407)'))
|
|
73
60
|
}
|
|
61
|
+
return
|
|
74
62
|
}
|
|
75
|
-
)
|
|
76
|
-
return
|
|
63
|
+
if (onHeaders) onHeaders.call(this, statusCode, data, resume)
|
|
77
64
|
}
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
|
|
66
|
+
// Rewrite request as an HTTP1 Proxy request, without tunneling.
|
|
67
|
+
const {
|
|
68
|
+
origin,
|
|
69
|
+
path = '/',
|
|
70
|
+
headers = {}
|
|
71
|
+
} = opts
|
|
72
|
+
|
|
73
|
+
opts.path = origin + path
|
|
74
|
+
|
|
75
|
+
if (!('host' in headers) && !('Host' in headers)) {
|
|
76
|
+
const { host } = new URL(origin)
|
|
77
|
+
headers.host = host
|
|
80
78
|
}
|
|
79
|
+
opts.headers = { ...this[kProxyHeaders], ...headers }
|
|
80
|
+
|
|
81
|
+
return this.#client[kDispatch](opts, handler)
|
|
82
|
+
}
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
async [kClose] () {
|
|
85
|
+
return this.#client.close()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async [kDestroy] (err) {
|
|
89
|
+
return this.#client.destroy(err)
|
|
83
90
|
}
|
|
84
91
|
}
|
|
92
|
+
|
|
85
93
|
class ProxyAgent extends DispatcherBase {
|
|
86
94
|
constructor (opts) {
|
|
87
95
|
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
|
|
@@ -104,6 +112,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
104
112
|
this[kRequestTls] = opts.requestTls
|
|
105
113
|
this[kProxyTls] = opts.proxyTls
|
|
106
114
|
this[kProxyHeaders] = opts.headers || {}
|
|
115
|
+
this[kTunnelProxy] = proxyTunnel
|
|
107
116
|
|
|
108
117
|
if (opts.auth && opts.token) {
|
|
109
118
|
throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token')
|
|
@@ -116,21 +125,25 @@ class ProxyAgent extends DispatcherBase {
|
|
|
116
125
|
this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
const factory = (!proxyTunnel && protocol === 'http:')
|
|
120
|
-
? (origin, options) => {
|
|
121
|
-
if (origin.protocol === 'http:') {
|
|
122
|
-
return new ProxyClient(origin, options)
|
|
123
|
-
}
|
|
124
|
-
return new Client(origin, options)
|
|
125
|
-
}
|
|
126
|
-
: undefined
|
|
127
|
-
|
|
128
128
|
const connect = buildConnector({ ...opts.proxyTls })
|
|
129
129
|
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
|
|
131
|
+
const agentFactory = opts.factory || defaultAgentFactory
|
|
132
|
+
const factory = (origin, options) => {
|
|
133
|
+
const { protocol } = new URL(origin)
|
|
134
|
+
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
|
|
135
|
+
return new Http1ProxyWrapper(this[kProxy].uri, {
|
|
136
|
+
headers: this[kProxyHeaders],
|
|
137
|
+
connect,
|
|
138
|
+
factory: agentFactory
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
return agentFactory(origin, options)
|
|
142
|
+
}
|
|
143
|
+
this[kClient] = clientFactory(url, { connect })
|
|
132
144
|
this[kAgent] = new Agent({
|
|
133
145
|
...opts,
|
|
146
|
+
factory,
|
|
134
147
|
connect: async (opts, callback) => {
|
|
135
148
|
let requestedPath = opts.host
|
|
136
149
|
if (!opts.port) {
|
|
@@ -185,10 +198,6 @@ class ProxyAgent extends DispatcherBase {
|
|
|
185
198
|
headers.host = host
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
if (!this.#shouldConnect(new URL(opts.origin))) {
|
|
189
|
-
opts.path = opts.origin + opts.path
|
|
190
|
-
}
|
|
191
|
-
|
|
192
201
|
return this[kAgent].dispatch(
|
|
193
202
|
{
|
|
194
203
|
...opts,
|
|
@@ -221,19 +230,6 @@ class ProxyAgent extends DispatcherBase {
|
|
|
221
230
|
await this[kAgent].destroy()
|
|
222
231
|
await this[kClient].destroy()
|
|
223
232
|
}
|
|
224
|
-
|
|
225
|
-
#shouldConnect (uri) {
|
|
226
|
-
if (typeof uri === 'string') {
|
|
227
|
-
uri = new URL(uri)
|
|
228
|
-
}
|
|
229
|
-
if (this[kTunnelProxy]) {
|
|
230
|
-
return true
|
|
231
|
-
}
|
|
232
|
-
if (uri.protocol !== 'http:' || this[kProxy].protocol !== 'http:') {
|
|
233
|
-
return true
|
|
234
|
-
}
|
|
235
|
-
return false
|
|
236
|
-
}
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
/**
|
|
@@ -133,6 +133,16 @@ class RedirectHandler {
|
|
|
133
133
|
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
|
134
134
|
const path = search ? `${pathname}${search}` : pathname
|
|
135
135
|
|
|
136
|
+
// Check for redirect loops by seeing if we've already visited this URL in our history
|
|
137
|
+
// This catches the case where Client/Pool try to handle cross-origin redirects but fail
|
|
138
|
+
// and keep redirecting to the same URL in an infinite loop
|
|
139
|
+
const redirectUrlString = `${origin}${path}`
|
|
140
|
+
for (const historyUrl of this.history) {
|
|
141
|
+
if (historyUrl.toString() === redirectUrlString) {
|
|
142
|
+
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.`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
136
146
|
// Remove headers referring to the original URL.
|
|
137
147
|
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
|
|
138
148
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
package/lib/interceptor/dump.js
CHANGED
package/lib/mock/mock-agent.js
CHANGED
|
@@ -17,7 +17,8 @@ const {
|
|
|
17
17
|
kMockAgentAddCallHistoryLog,
|
|
18
18
|
kMockAgentMockCallHistoryInstance,
|
|
19
19
|
kMockAgentAcceptsNonStandardSearchParameters,
|
|
20
|
-
kMockCallHistoryAddLog
|
|
20
|
+
kMockCallHistoryAddLog,
|
|
21
|
+
kIgnoreTrailingSlash
|
|
21
22
|
} = require('./mock-symbols')
|
|
22
23
|
const MockClient = require('./mock-client')
|
|
23
24
|
const MockPool = require('./mock-pool')
|
|
@@ -37,6 +38,7 @@ class MockAgent extends Dispatcher {
|
|
|
37
38
|
this[kIsMockActive] = true
|
|
38
39
|
this[kMockAgentIsCallHistoryEnabled] = mockOptions?.enableCallHistory ?? false
|
|
39
40
|
this[kMockAgentAcceptsNonStandardSearchParameters] = mockOptions?.acceptNonStandardSearchParameters ?? false
|
|
41
|
+
this[kIgnoreTrailingSlash] = mockOptions?.ignoreTrailingSlash ?? false
|
|
40
42
|
|
|
41
43
|
// Instantiate Agent and encapsulate
|
|
42
44
|
if (opts?.agent && typeof opts.agent.dispatch !== 'function') {
|
|
@@ -54,11 +56,15 @@ class MockAgent extends Dispatcher {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
get (origin) {
|
|
57
|
-
|
|
59
|
+
const originKey = this[kIgnoreTrailingSlash]
|
|
60
|
+
? origin.replace(/\/$/, '')
|
|
61
|
+
: origin
|
|
62
|
+
|
|
63
|
+
let dispatcher = this[kMockAgentGet](originKey)
|
|
58
64
|
|
|
59
65
|
if (!dispatcher) {
|
|
60
|
-
dispatcher = this[kFactory](
|
|
61
|
-
this[kMockAgentSet](
|
|
66
|
+
dispatcher = this[kFactory](originKey)
|
|
67
|
+
this[kMockAgentSet](originKey, dispatcher)
|
|
62
68
|
}
|
|
63
69
|
return dispatcher
|
|
64
70
|
}
|