undici 7.11.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 +15 -11
- package/docs/docs/api/DiagnosticsChannel.md +7 -4
- package/docs/docs/api/Dispatcher.md +2 -2
- package/docs/docs/api/ProxyAgent.md +1 -1
- package/docs/docs/api/SnapshotAgent.md +616 -0
- package/docs/docs/api/WebSocket.md +27 -0
- package/index.js +5 -1
- package/lib/api/readable.js +49 -29
- package/lib/core/request.js +6 -1
- package/lib/core/tree.js +1 -1
- package/lib/core/util.js +0 -1
- package/lib/dispatcher/client-h1.js +8 -17
- package/lib/dispatcher/proxy-agent.js +67 -71
- package/lib/handler/cache-handler.js +4 -1
- package/lib/handler/redirect-handler.js +12 -2
- package/lib/interceptor/cache.js +2 -2
- package/lib/interceptor/dump.js +2 -1
- package/lib/interceptor/redirect.js +1 -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/util/cache.js +1 -1
- package/lib/util/promise.js +28 -0
- package/lib/web/cache/cache.js +10 -8
- package/lib/web/fetch/body.js +35 -24
- package/lib/web/fetch/formdata-parser.js +0 -3
- package/lib/web/fetch/formdata.js +0 -4
- package/lib/web/fetch/index.js +221 -225
- package/lib/web/fetch/request.js +15 -7
- package/lib/web/fetch/response.js +5 -3
- package/lib/web/fetch/util.js +21 -23
- package/lib/web/webidl/index.js +1 -1
- package/lib/web/websocket/connection.js +0 -9
- package/lib/web/websocket/receiver.js +2 -12
- package/lib/web/websocket/stream/websocketstream.js +7 -4
- package/lib/web/websocket/websocket.js +57 -1
- package/package.json +2 -2
- 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/types/webidl.d.ts +10 -0
- package/types/websocket.d.ts +2 -0
- package/lib/web/fetch/dispatcher-weakref.js +0 -5
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
|
|
|
@@ -89,16 +96,14 @@ class BodyReadable extends Readable {
|
|
|
89
96
|
// promise (i.e micro tick) for installing an 'error' listener will
|
|
90
97
|
// never get a chance and will always encounter an unhandled exception.
|
|
91
98
|
if (!this[kUsed]) {
|
|
92
|
-
setImmediate(
|
|
93
|
-
callback(err)
|
|
94
|
-
})
|
|
99
|
+
setImmediate(callback, err)
|
|
95
100
|
} else {
|
|
96
101
|
callback(err)
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
|
-
* @param {string} event
|
|
106
|
+
* @param {string|symbol} event
|
|
102
107
|
* @param {(...args: any[]) => void} listener
|
|
103
108
|
* @returns {this}
|
|
104
109
|
*/
|
|
@@ -111,7 +116,7 @@ class BodyReadable extends Readable {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
/**
|
|
114
|
-
* @param {string} event
|
|
119
|
+
* @param {string|symbol} event
|
|
115
120
|
* @param {(...args: any[]) => void} listener
|
|
116
121
|
* @returns {this}
|
|
117
122
|
*/
|
|
@@ -149,12 +154,14 @@ class BodyReadable extends Readable {
|
|
|
149
154
|
* @returns {boolean}
|
|
150
155
|
*/
|
|
151
156
|
push (chunk) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
163
|
}
|
|
164
|
+
|
|
158
165
|
return super.push(chunk)
|
|
159
166
|
}
|
|
160
167
|
|
|
@@ -340,9 +347,23 @@ function isUnusable (bodyReadable) {
|
|
|
340
347
|
return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
|
|
341
348
|
}
|
|
342
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
|
+
*/
|
|
343
364
|
/**
|
|
344
365
|
* @typedef {object} Consume
|
|
345
|
-
* @property {
|
|
366
|
+
* @property {ConsumeType} type
|
|
346
367
|
* @property {BodyReadable} stream
|
|
347
368
|
* @property {((value?: any) => void)} resolve
|
|
348
369
|
* @property {((err: Error) => void)} reject
|
|
@@ -351,9 +372,10 @@ function isUnusable (bodyReadable) {
|
|
|
351
372
|
*/
|
|
352
373
|
|
|
353
374
|
/**
|
|
375
|
+
* @template {ConsumeType} T
|
|
354
376
|
* @param {BodyReadable} stream
|
|
355
|
-
* @param {
|
|
356
|
-
* @returns {Promise<
|
|
377
|
+
* @param {T} type
|
|
378
|
+
* @returns {Promise<ConsumeReturnType<T>>}
|
|
357
379
|
*/
|
|
358
380
|
function consume (stream, type) {
|
|
359
381
|
assert(!stream[kConsume])
|
|
@@ -363,9 +385,7 @@ function consume (stream, type) {
|
|
|
363
385
|
const rState = stream._readableState
|
|
364
386
|
if (rState.destroyed && rState.closeEmitted === false) {
|
|
365
387
|
stream
|
|
366
|
-
.on('error',
|
|
367
|
-
reject(err)
|
|
368
|
-
})
|
|
388
|
+
.on('error', reject)
|
|
369
389
|
.on('close', () => {
|
|
370
390
|
reject(new TypeError('unusable'))
|
|
371
391
|
})
|
|
@@ -440,7 +460,7 @@ function consumeStart (consume) {
|
|
|
440
460
|
/**
|
|
441
461
|
* @param {Buffer[]} chunks
|
|
442
462
|
* @param {number} length
|
|
443
|
-
* @param {BufferEncoding} encoding
|
|
463
|
+
* @param {BufferEncoding} [encoding='utf8']
|
|
444
464
|
* @returns {string}
|
|
445
465
|
*/
|
|
446
466
|
function chunksDecode (chunks, length, encoding) {
|
package/lib/core/request.js
CHANGED
|
@@ -42,7 +42,8 @@ class Request {
|
|
|
42
42
|
reset,
|
|
43
43
|
expectContinue,
|
|
44
44
|
servername,
|
|
45
|
-
throwOnError
|
|
45
|
+
throwOnError,
|
|
46
|
+
maxRedirections
|
|
46
47
|
}, handler) {
|
|
47
48
|
if (typeof path !== 'string') {
|
|
48
49
|
throw new InvalidArgumentError('path must be a string')
|
|
@@ -86,6 +87,10 @@ class Request {
|
|
|
86
87
|
throw new InvalidArgumentError('invalid throwOnError')
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
if (maxRedirections != null && maxRedirections !== 0) {
|
|
91
|
+
throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor')
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
this.headersTimeout = headersTimeout
|
|
90
95
|
|
|
91
96
|
this.bodyTimeout = bodyTimeout
|
package/lib/core/tree.js
CHANGED
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')
|
|
@@ -60,12 +60,12 @@ const removeAllListeners = util.removeAllListeners
|
|
|
60
60
|
|
|
61
61
|
let extractBody
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
function lazyllhttp () {
|
|
64
64
|
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
|
|
65
65
|
|
|
66
66
|
let mod
|
|
67
67
|
try {
|
|
68
|
-
mod =
|
|
68
|
+
mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
|
|
69
69
|
} catch (e) {
|
|
70
70
|
/* istanbul ignore next */
|
|
71
71
|
|
|
@@ -73,10 +73,10 @@ async function lazyllhttp () {
|
|
|
73
73
|
// being enabled, but the occurring of this other error
|
|
74
74
|
// * https://github.com/emscripten-core/emscripten/issues/11495
|
|
75
75
|
// got me to remove that check to avoid breaking Node 12.
|
|
76
|
-
mod =
|
|
76
|
+
mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
return
|
|
79
|
+
return new WebAssembly.Instance(mod, {
|
|
80
80
|
env: {
|
|
81
81
|
/**
|
|
82
82
|
* @param {number} p
|
|
@@ -165,11 +165,6 @@ async function lazyllhttp () {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
let llhttpInstance = null
|
|
168
|
-
/**
|
|
169
|
-
* @type {Promise<WebAssembly.Instance>|null}
|
|
170
|
-
*/
|
|
171
|
-
let llhttpPromise = lazyllhttp()
|
|
172
|
-
llhttpPromise.catch()
|
|
173
168
|
|
|
174
169
|
/**
|
|
175
170
|
* @type {Parser|null}
|
|
@@ -732,7 +727,7 @@ class Parser {
|
|
|
732
727
|
// We must wait a full event loop cycle to reuse this socket to make sure
|
|
733
728
|
// that non-spec compliant servers are not closing the connection even if they
|
|
734
729
|
// said they won't.
|
|
735
|
-
setImmediate(
|
|
730
|
+
setImmediate(client[kResume])
|
|
736
731
|
} else {
|
|
737
732
|
client[kResume]()
|
|
738
733
|
}
|
|
@@ -769,11 +764,7 @@ async function connectH1 (client, socket) {
|
|
|
769
764
|
client[kSocket] = socket
|
|
770
765
|
|
|
771
766
|
if (!llhttpInstance) {
|
|
772
|
-
|
|
773
|
-
socket.on('error', noop)
|
|
774
|
-
llhttpInstance = await llhttpPromise
|
|
775
|
-
llhttpPromise = null
|
|
776
|
-
socket.off('error', noop)
|
|
767
|
+
llhttpInstance = lazyllhttp()
|
|
777
768
|
}
|
|
778
769
|
|
|
779
770
|
if (socket.errored) {
|
|
@@ -1297,9 +1288,9 @@ function writeStream (abort, body, client, request, socket, contentLength, heade
|
|
|
1297
1288
|
.on('error', onFinished)
|
|
1298
1289
|
|
|
1299
1290
|
if (body.errorEmitted ?? body.errored) {
|
|
1300
|
-
setImmediate(
|
|
1291
|
+
setImmediate(onFinished, body.errored)
|
|
1301
1292
|
} else if (body.endEmitted ?? body.readableEnded) {
|
|
1302
|
-
setImmediate(
|
|
1293
|
+
setImmediate(onFinished, null)
|
|
1303
1294
|
}
|
|
1304
1295
|
|
|
1305
1296
|
if (body.closeEmitted ?? body.closed) {
|
|
@@ -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
|
/**
|
|
@@ -241,7 +241,10 @@ class CacheHandler {
|
|
|
241
241
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
|
|
242
242
|
*/
|
|
243
243
|
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
|
|
244
|
-
|
|
244
|
+
// Allow caching for status codes 200 and 307 (original behavior)
|
|
245
|
+
// Also allow caching for other status codes that are heuristically cacheable
|
|
246
|
+
// when they have explicit cache directives
|
|
247
|
+
if (statusCode !== 200 && statusCode !== 307 && !HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)) {
|
|
245
248
|
return false
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -42,7 +42,8 @@ class RedirectHandler {
|
|
|
42
42
|
|
|
43
43
|
this.dispatch = dispatch
|
|
44
44
|
this.location = null
|
|
45
|
-
|
|
45
|
+
const { maxRedirections: _, ...cleanOpts } = opts
|
|
46
|
+
this.opts = cleanOpts // opts must be a copy, exclude maxRedirections
|
|
46
47
|
this.maxRedirections = maxRedirections
|
|
47
48
|
this.handler = handler
|
|
48
49
|
this.history = []
|
|
@@ -132,13 +133,22 @@ class RedirectHandler {
|
|
|
132
133
|
const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin)))
|
|
133
134
|
const path = search ? `${pathname}${search}` : pathname
|
|
134
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
|
+
|
|
135
146
|
// Remove headers referring to the original URL.
|
|
136
147
|
// By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers.
|
|
137
148
|
// https://tools.ietf.org/html/rfc7231#section-6.4
|
|
138
149
|
this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
|
|
139
150
|
this.opts.path = path
|
|
140
151
|
this.opts.origin = origin
|
|
141
|
-
this.opts.maxRedirections = 0
|
|
142
152
|
this.opts.query = null
|
|
143
153
|
}
|
|
144
154
|
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -301,11 +301,11 @@ module.exports = (opts = {}) => {
|
|
|
301
301
|
assertCacheMethods(methods, 'opts.methods')
|
|
302
302
|
|
|
303
303
|
if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
|
|
304
|
-
throw new TypeError(`
|
|
304
|
+
throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
|
|
308
|
-
throw new TypeError(`
|
|
308
|
+
throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
const globalOpts = {
|
package/lib/interceptor/dump.js
CHANGED
|
@@ -11,7 +11,7 @@ function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }
|
|
|
11
11
|
return dispatch(opts, handler)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const dispatchOpts = { ...rest
|
|
14
|
+
const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting.
|
|
15
15
|
const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
|
|
16
16
|
return dispatch(dispatchOpts, redirectHandler)
|
|
17
17
|
}
|
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
|
}
|