undici 7.0.0-alpha.3 → 7.0.0-alpha.5
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 +2 -1
- package/docs/docs/api/Agent.md +14 -14
- package/docs/docs/api/BalancedPool.md +16 -16
- package/docs/docs/api/CacheStore.md +17 -14
- package/docs/docs/api/Client.md +11 -11
- package/docs/docs/api/Dispatcher.md +30 -10
- package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
- package/docs/docs/api/MockAgent.md +3 -3
- package/docs/docs/api/MockClient.md +5 -5
- package/docs/docs/api/MockPool.md +2 -2
- package/docs/docs/api/Pool.md +15 -15
- package/docs/docs/api/PoolStats.md +1 -1
- package/docs/docs/api/ProxyAgent.md +3 -3
- package/docs/docs/api/RetryHandler.md +2 -2
- package/docs/docs/api/WebSocket.md +1 -1
- package/docs/docs/api/api-lifecycle.md +11 -11
- package/docs/docs/best-practices/mocking-request.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/index.d.ts +1 -1
- package/index.js +2 -1
- package/lib/api/api-request.js +1 -1
- package/lib/cache/memory-cache-store.js +106 -342
- package/lib/core/connect.js +5 -0
- package/lib/core/request.js +2 -2
- package/lib/core/util.js +13 -40
- package/lib/dispatcher/client-h2.js +53 -33
- package/lib/handler/cache-handler.js +126 -85
- package/lib/handler/cache-revalidation-handler.js +45 -13
- package/lib/handler/redirect-handler.js +5 -3
- package/lib/handler/retry-handler.js +3 -3
- package/lib/interceptor/cache.js +213 -92
- package/lib/interceptor/dns.js +71 -48
- package/lib/util/cache.js +73 -13
- package/lib/util/timers.js +19 -1
- package/lib/web/cookies/index.js +12 -1
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/fetch/body.js +1 -5
- package/lib/web/fetch/formdata-parser.js +70 -43
- package/lib/web/fetch/headers.js +1 -1
- package/lib/web/fetch/index.js +4 -6
- package/lib/web/fetch/webidl.js +12 -4
- package/package.json +2 -3
- package/types/cache-interceptor.d.ts +51 -54
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +1 -1
- package/types/index.d.ts +0 -1
- package/types/interceptors.d.ts +0 -1
|
@@ -75,7 +75,8 @@ class RedirectHandler {
|
|
|
75
75
|
this.opts.body &&
|
|
76
76
|
typeof this.opts.body !== 'string' &&
|
|
77
77
|
!ArrayBuffer.isView(this.opts.body) &&
|
|
78
|
-
util.isIterable(this.opts.body)
|
|
78
|
+
util.isIterable(this.opts.body) &&
|
|
79
|
+
!util.isFormDataLike(this.opts.body)
|
|
79
80
|
) {
|
|
80
81
|
// TODO: Should we allow re-using iterable if !this.opts.idempotent
|
|
81
82
|
// or through some other flag?
|
|
@@ -227,9 +228,10 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
} else if (headers && typeof headers === 'object') {
|
|
230
|
-
|
|
231
|
+
const entries = typeof headers[Symbol.iterator] === 'function' ? headers : Object.entries(headers)
|
|
232
|
+
for (const [key, value] of entries) {
|
|
231
233
|
if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) {
|
|
232
|
-
ret.push(key,
|
|
234
|
+
ret.push(key, value)
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
} else {
|
|
@@ -229,7 +229,7 @@ class RetryHandler {
|
|
|
229
229
|
return false
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
const { start, size, end = size } = contentRange
|
|
232
|
+
const { start, size, end = size - 1 } = contentRange
|
|
233
233
|
|
|
234
234
|
assert(this.start === start, 'content-range mismatch')
|
|
235
235
|
assert(this.end == null || this.end === end, 'content-range mismatch')
|
|
@@ -252,7 +252,7 @@ class RetryHandler {
|
|
|
252
252
|
)
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
const { start, size, end = size } = range
|
|
255
|
+
const { start, size, end = size - 1 } = range
|
|
256
256
|
assert(
|
|
257
257
|
start != null && Number.isFinite(start),
|
|
258
258
|
'content-range mismatch'
|
|
@@ -266,7 +266,7 @@ class RetryHandler {
|
|
|
266
266
|
// We make our best to checkpoint the body for further range headers
|
|
267
267
|
if (this.end == null) {
|
|
268
268
|
const contentLength = headers['content-length']
|
|
269
|
-
this.end = contentLength != null ? Number(contentLength) : null
|
|
269
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
assert(Number.isFinite(this.start))
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -1,13 +1,88 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { Readable } = require('node:stream')
|
|
3
5
|
const util = require('../core/util')
|
|
4
6
|
const CacheHandler = require('../handler/cache-handler')
|
|
5
7
|
const MemoryCacheStore = require('../cache/memory-cache-store')
|
|
6
8
|
const CacheRevalidationHandler = require('../handler/cache-revalidation-handler')
|
|
7
|
-
const { assertCacheStore, assertCacheMethods } = require('../util/cache.js')
|
|
9
|
+
const { assertCacheStore, assertCacheMethods, makeCacheKey, parseCacheControlHeader } = require('../util/cache.js')
|
|
10
|
+
const { nowAbsolute } = require('../util/timers.js')
|
|
8
11
|
|
|
9
12
|
const AGE_HEADER = Buffer.from('age')
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
|
|
16
|
+
*/
|
|
17
|
+
function sendGatewayTimeout (handler) {
|
|
18
|
+
let aborted = false
|
|
19
|
+
try {
|
|
20
|
+
if (typeof handler.onConnect === 'function') {
|
|
21
|
+
handler.onConnect(() => {
|
|
22
|
+
aborted = true
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (aborted) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof handler.onHeaders === 'function') {
|
|
31
|
+
handler.onHeaders(504, [], () => {}, 'Gateway Timeout')
|
|
32
|
+
if (aborted) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof handler.onComplete === 'function') {
|
|
38
|
+
handler.onComplete([])
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (typeof handler.onError === 'function') {
|
|
42
|
+
handler.onError(err)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
49
|
+
* @param {number} age
|
|
50
|
+
* @param {import('../util/cache.js').CacheControlDirectives | undefined} cacheControlDirectives
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
function needsRevalidation (result, age, cacheControlDirectives) {
|
|
54
|
+
if (cacheControlDirectives?.['no-cache']) {
|
|
55
|
+
// Always revalidate requests with the no-cache directive
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const now = nowAbsolute()
|
|
60
|
+
if (now > result.staleAt) {
|
|
61
|
+
// Response is stale
|
|
62
|
+
if (cacheControlDirectives?.['max-stale']) {
|
|
63
|
+
// There's a threshold where we can serve stale responses, let's see if
|
|
64
|
+
// we're in it
|
|
65
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale
|
|
66
|
+
const gracePeriod = result.staleAt + (cacheControlDirectives['max-stale'] * 1000)
|
|
67
|
+
return now > gracePeriod
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cacheControlDirectives?.['min-fresh']) {
|
|
74
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.3
|
|
75
|
+
|
|
76
|
+
// At this point, staleAt is always > now
|
|
77
|
+
const timeLeftTillStale = result.staleAt - now
|
|
78
|
+
const threshold = cacheControlDirectives['min-fresh'] * 1000
|
|
79
|
+
|
|
80
|
+
return timeLeftTillStale <= threshold
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
11
86
|
/**
|
|
12
87
|
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} [opts]
|
|
13
88
|
* @returns {import('../../types/dispatcher.d.ts').default.DispatcherComposeInterceptor}
|
|
@@ -34,136 +109,182 @@ module.exports = (opts = {}) => {
|
|
|
34
109
|
|
|
35
110
|
return dispatch => {
|
|
36
111
|
return (opts, handler) => {
|
|
112
|
+
// TODO (fix): What if e.g. opts.headers has if-modified-since header? Or other headers
|
|
113
|
+
// that make things ambigious?
|
|
114
|
+
|
|
37
115
|
if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) {
|
|
38
116
|
// Not a method we want to cache or we don't have the origin, skip
|
|
39
117
|
return dispatch(opts, handler)
|
|
40
118
|
}
|
|
41
119
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
46
|
-
}
|
|
120
|
+
const requestCacheControl = opts.headers?.['cache-control']
|
|
121
|
+
? parseCacheControlHeader(opts.headers['cache-control'])
|
|
122
|
+
: undefined
|
|
47
123
|
|
|
48
|
-
|
|
124
|
+
if (requestCacheControl?.['no-store']) {
|
|
125
|
+
return dispatch(opts, handler)
|
|
126
|
+
}
|
|
49
127
|
|
|
50
128
|
/**
|
|
51
|
-
* @
|
|
52
|
-
* @param {import('../../types/cache-interceptor.d.ts').default.CacheStoreValue} value
|
|
129
|
+
* @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
|
|
53
130
|
*/
|
|
54
|
-
const
|
|
55
|
-
const ac = new AbortController()
|
|
56
|
-
const signal = ac.signal
|
|
131
|
+
const cacheKey = makeCacheKey(opts)
|
|
57
132
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
// TODO (perf): For small entries support returning a Buffer instead of a stream.
|
|
134
|
+
// Maybe store should return { staleAt, headers, body, etc... } instead of a stream + stream.value?
|
|
135
|
+
// Where body can be a Buffer, string, stream or blob?
|
|
136
|
+
const result = store.get(cacheKey)
|
|
137
|
+
if (!result) {
|
|
138
|
+
if (requestCacheControl?.['only-if-cached']) {
|
|
139
|
+
// We only want cached responses
|
|
140
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
|
|
141
|
+
sendGatewayTimeout(handler)
|
|
142
|
+
return true
|
|
64
143
|
}
|
|
65
144
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
handler.onError(err)
|
|
69
|
-
onErrorCalled = true
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
if (typeof handler.onConnect === 'function') {
|
|
75
|
-
handler.onConnect(ac.abort)
|
|
76
|
-
signal.throwIfAborted()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (typeof handler.onHeaders === 'function') {
|
|
80
|
-
// Add the age header
|
|
81
|
-
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
82
|
-
const age = Math.round((Date.now() - value.cachedAt) / 1000)
|
|
145
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
146
|
+
}
|
|
83
147
|
|
|
84
|
-
|
|
148
|
+
/**
|
|
149
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
150
|
+
* @param {number} age
|
|
151
|
+
*/
|
|
152
|
+
const respondWithCachedValue = ({ rawHeaders, statusCode, statusMessage, body }, age) => {
|
|
153
|
+
const stream = util.isStream(body)
|
|
154
|
+
? body
|
|
155
|
+
: Readable.from(body ?? [])
|
|
85
156
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
157
|
+
assert(!stream.destroyed, 'stream should not be destroyed')
|
|
158
|
+
assert(!stream.readableDidRead, 'stream should not be readableDidRead')
|
|
89
159
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
160
|
+
stream
|
|
161
|
+
.on('error', function (err) {
|
|
162
|
+
if (!this.readableEnded) {
|
|
163
|
+
if (typeof handler.onError === 'function') {
|
|
164
|
+
handler.onError(err)
|
|
165
|
+
} else {
|
|
166
|
+
throw err
|
|
167
|
+
}
|
|
94
168
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
stream.pause()
|
|
100
|
-
}
|
|
101
|
-
})
|
|
169
|
+
})
|
|
170
|
+
.on('close', function () {
|
|
171
|
+
if (!this.errored && typeof handler.onComplete === 'function') {
|
|
172
|
+
handler.onComplete([])
|
|
102
173
|
}
|
|
174
|
+
})
|
|
103
175
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
176
|
+
if (typeof handler.onConnect === 'function') {
|
|
177
|
+
handler.onConnect((err) => {
|
|
178
|
+
stream.destroy(err)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (stream.destroyed) {
|
|
182
|
+
return
|
|
109
183
|
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof handler.onHeaders === 'function') {
|
|
187
|
+
// Add the age header
|
|
188
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-age
|
|
189
|
+
|
|
190
|
+
// TODO (fix): What if rawHeaders already contains age header?
|
|
191
|
+
rawHeaders = [...rawHeaders, AGE_HEADER, Buffer.from(`${age}`)]
|
|
192
|
+
|
|
193
|
+
if (handler.onHeaders(statusCode, rawHeaders, () => stream?.resume(), statusMessage) === false) {
|
|
194
|
+
stream.pause()
|
|
115
195
|
}
|
|
116
196
|
}
|
|
197
|
+
|
|
198
|
+
if (opts.method === 'HEAD') {
|
|
199
|
+
stream.destroy()
|
|
200
|
+
} else {
|
|
201
|
+
stream.on('data', function (chunk) {
|
|
202
|
+
if (typeof handler.onData === 'function' && !handler.onData(chunk)) {
|
|
203
|
+
stream.pause()
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
117
207
|
}
|
|
118
208
|
|
|
119
209
|
/**
|
|
120
|
-
* @param {import('../../types/cache-interceptor.d.ts').default.
|
|
210
|
+
* @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
|
|
121
211
|
*/
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
// Request isn't cached
|
|
125
|
-
return dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
126
|
-
}
|
|
212
|
+
const handleResult = (result) => {
|
|
213
|
+
// TODO (perf): Readable.from path can be optimized...
|
|
127
214
|
|
|
128
|
-
|
|
215
|
+
if (!result.body && opts.method !== 'HEAD') {
|
|
216
|
+
throw new Error('stream is undefined but method isn\'t HEAD')
|
|
217
|
+
}
|
|
129
218
|
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
219
|
+
const age = Math.round((nowAbsolute() - result.cachedAt) / 1000)
|
|
220
|
+
if (requestCacheControl?.['max-age'] && age >= requestCacheControl['max-age']) {
|
|
221
|
+
// Response is considered expired for this specific request
|
|
222
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1
|
|
223
|
+
return dispatch(opts, handler)
|
|
133
224
|
}
|
|
134
225
|
|
|
135
226
|
// Check if the response is stale
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
dispatch(opts, new CacheHandler(globalOpts, opts, handler))
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!opts.headers) {
|
|
146
|
-
opts.headers = {}
|
|
227
|
+
if (needsRevalidation(result, age, requestCacheControl)) {
|
|
228
|
+
if (util.isStream(opts.body) && util.bodyLength(opts.body) !== 0) {
|
|
229
|
+
// If body is is stream we can't revalidate...
|
|
230
|
+
// TODO (fix): This could be less strict...
|
|
231
|
+
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
147
232
|
}
|
|
148
233
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
234
|
+
// We need to revalidate the response
|
|
235
|
+
return dispatch(
|
|
236
|
+
{
|
|
237
|
+
...opts,
|
|
238
|
+
headers: {
|
|
239
|
+
...opts.headers,
|
|
240
|
+
'if-modified-since': new Date(result.cachedAt).toUTCString(),
|
|
241
|
+
etag: result.etag
|
|
242
|
+
}
|
|
243
|
+
},
|
|
154
244
|
new CacheRevalidationHandler(
|
|
155
|
-
() =>
|
|
156
|
-
|
|
245
|
+
(success) => {
|
|
246
|
+
if (success) {
|
|
247
|
+
respondWithCachedValue(result, age)
|
|
248
|
+
} else if (util.isStream(result.body)) {
|
|
249
|
+
result.body.on('error', () => {}).destroy()
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
new CacheHandler(globalOpts, cacheKey, handler)
|
|
157
253
|
)
|
|
158
254
|
)
|
|
159
|
-
|
|
160
|
-
return
|
|
161
255
|
}
|
|
162
256
|
|
|
163
|
-
|
|
257
|
+
// Dump request body.
|
|
258
|
+
if (util.isStream(opts.body)) {
|
|
259
|
+
opts.body.on('error', () => {}).destroy()
|
|
260
|
+
}
|
|
261
|
+
respondWithCachedValue(result, age)
|
|
164
262
|
}
|
|
165
263
|
|
|
166
|
-
|
|
264
|
+
if (typeof result.then === 'function') {
|
|
265
|
+
result.then((result) => {
|
|
266
|
+
if (!result) {
|
|
267
|
+
if (requestCacheControl?.['only-if-cached']) {
|
|
268
|
+
// We only want cached responses
|
|
269
|
+
// https://www.rfc-editor.org/rfc/rfc9111.html#name-only-if-cached
|
|
270
|
+
sendGatewayTimeout(handler)
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
|
|
275
|
+
} else {
|
|
276
|
+
handleResult(result)
|
|
277
|
+
}
|
|
278
|
+
}, err => {
|
|
279
|
+
if (typeof handler.onError === 'function') {
|
|
280
|
+
handler.onError(err)
|
|
281
|
+
} else {
|
|
282
|
+
throw err
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
} else {
|
|
286
|
+
handleResult(result)
|
|
287
|
+
}
|
|
167
288
|
|
|
168
289
|
return true
|
|
169
290
|
}
|
package/lib/interceptor/dns.js
CHANGED
|
@@ -13,7 +13,6 @@ class DNSInstance {
|
|
|
13
13
|
affinity = null
|
|
14
14
|
lookup = null
|
|
15
15
|
pick = null
|
|
16
|
-
lastIpFamily = null
|
|
17
16
|
|
|
18
17
|
constructor (opts) {
|
|
19
18
|
this.#maxTTL = opts.maxTTL
|
|
@@ -61,16 +60,23 @@ class DNSInstance {
|
|
|
61
60
|
const ip = this.pick(
|
|
62
61
|
origin,
|
|
63
62
|
records,
|
|
64
|
-
|
|
65
|
-
// otherwise let it go through normal flow
|
|
66
|
-
!newOpts.dualStack && newOpts.affinity
|
|
63
|
+
newOpts.affinity
|
|
67
64
|
)
|
|
68
65
|
|
|
66
|
+
let port
|
|
67
|
+
if (typeof ip.port === 'number') {
|
|
68
|
+
port = `:${ip.port}`
|
|
69
|
+
} else if (origin.port !== '') {
|
|
70
|
+
port = `:${origin.port}`
|
|
71
|
+
} else {
|
|
72
|
+
port = ''
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
cb(
|
|
70
76
|
null,
|
|
71
77
|
`${origin.protocol}//${
|
|
72
78
|
ip.family === 6 ? `[${ip.address}]` : ip.address
|
|
73
|
-
}${
|
|
79
|
+
}${port}`
|
|
74
80
|
)
|
|
75
81
|
})
|
|
76
82
|
} else {
|
|
@@ -78,9 +84,7 @@ class DNSInstance {
|
|
|
78
84
|
const ip = this.pick(
|
|
79
85
|
origin,
|
|
80
86
|
ips,
|
|
81
|
-
|
|
82
|
-
// otherwise let it go through normal flow
|
|
83
|
-
!newOpts.dualStack && newOpts.affinity
|
|
87
|
+
newOpts.affinity
|
|
84
88
|
)
|
|
85
89
|
|
|
86
90
|
// If no IPs we lookup - deleting old records
|
|
@@ -90,11 +94,20 @@ class DNSInstance {
|
|
|
90
94
|
return
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
let port
|
|
98
|
+
if (typeof ip.port === 'number') {
|
|
99
|
+
port = `:${ip.port}`
|
|
100
|
+
} else if (origin.port !== '') {
|
|
101
|
+
port = `:${origin.port}`
|
|
102
|
+
} else {
|
|
103
|
+
port = ''
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
cb(
|
|
94
107
|
null,
|
|
95
108
|
`${origin.protocol}//${
|
|
96
109
|
ip.family === 6 ? `[${ip.address}]` : ip.address
|
|
97
|
-
}${
|
|
110
|
+
}${port}`
|
|
98
111
|
)
|
|
99
112
|
}
|
|
100
113
|
}
|
|
@@ -102,7 +115,11 @@ class DNSInstance {
|
|
|
102
115
|
#defaultLookup (origin, opts, cb) {
|
|
103
116
|
lookup(
|
|
104
117
|
origin.hostname,
|
|
105
|
-
{
|
|
118
|
+
{
|
|
119
|
+
all: true,
|
|
120
|
+
family: this.dualStack === false ? this.affinity : 0,
|
|
121
|
+
order: 'ipv4first'
|
|
122
|
+
},
|
|
106
123
|
(err, addresses) => {
|
|
107
124
|
if (err) {
|
|
108
125
|
return cb(err)
|
|
@@ -111,15 +128,9 @@ class DNSInstance {
|
|
|
111
128
|
const results = new Map()
|
|
112
129
|
|
|
113
130
|
for (const addr of addresses) {
|
|
114
|
-
const record = {
|
|
115
|
-
address: addr.address,
|
|
116
|
-
ttl: opts.maxTTL,
|
|
117
|
-
family: addr.family
|
|
118
|
-
}
|
|
119
|
-
|
|
120
131
|
// On linux we found duplicates, we attempt to remove them with
|
|
121
132
|
// the latest record
|
|
122
|
-
results.set(`${
|
|
133
|
+
results.set(`${addr.address}:${addr.family}`, addr)
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
cb(null, results.values())
|
|
@@ -129,36 +140,36 @@ class DNSInstance {
|
|
|
129
140
|
|
|
130
141
|
#defaultPick (origin, hostnameRecords, affinity) {
|
|
131
142
|
let ip = null
|
|
132
|
-
const { records, offset
|
|
133
|
-
|
|
143
|
+
const { records, offset } = hostnameRecords
|
|
144
|
+
|
|
145
|
+
let family
|
|
146
|
+
if (this.dualStack) {
|
|
147
|
+
if (affinity == null) {
|
|
148
|
+
// Balance between ip families
|
|
149
|
+
if (offset == null || offset === maxInt) {
|
|
150
|
+
hostnameRecords.offset = 0
|
|
151
|
+
affinity = 4
|
|
152
|
+
} else {
|
|
153
|
+
hostnameRecords.offset++
|
|
154
|
+
affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4
|
|
155
|
+
}
|
|
156
|
+
}
|
|
134
157
|
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
if (records[affinity] != null && records[affinity].ips.length > 0) {
|
|
159
|
+
family = records[affinity]
|
|
160
|
+
} else {
|
|
161
|
+
family = records[affinity === 4 ? 6 : 4]
|
|
162
|
+
}
|
|
137
163
|
} else {
|
|
138
|
-
|
|
164
|
+
family = records[affinity]
|
|
139
165
|
}
|
|
140
166
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
const newIpFamily = (newOffset & 1) === 1 ? 4 : 6
|
|
144
|
-
const family =
|
|
145
|
-
this.dualStack === false
|
|
146
|
-
? records[this.affinity] // If dual-stack is disabled, we pick the default affiniy
|
|
147
|
-
: records[affinity] ?? records[newIpFamily]
|
|
148
|
-
|
|
149
|
-
// If no IPs and we have tried both families or dual stack is disabled, we return null
|
|
150
|
-
if (
|
|
151
|
-
(family == null || family.ips.length === 0) &&
|
|
152
|
-
// eslint-disable-next-line eqeqeq
|
|
153
|
-
(this.dualStack === false || this.lastIpFamily != newIpFamily)
|
|
154
|
-
) {
|
|
167
|
+
// If no IPs we return null
|
|
168
|
+
if (family == null || family.ips.length === 0) {
|
|
155
169
|
return ip
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
family.offset
|
|
159
|
-
hostnameRecords.offset = newOffset
|
|
160
|
-
|
|
161
|
-
if (family.offset === maxInt) {
|
|
172
|
+
if (family.offset == null || family.offset === maxInt) {
|
|
162
173
|
family.offset = 0
|
|
163
174
|
} else {
|
|
164
175
|
family.offset++
|
|
@@ -171,24 +182,28 @@ class DNSInstance {
|
|
|
171
182
|
return ip
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
// Record TTL is already in ms
|
|
176
|
-
if (ip.timestamp != null && timestamp - ip.timestamp > ip.ttl) {
|
|
185
|
+
if (Date.now() - ip.timestamp > ip.ttl) { // record TTL is already in ms
|
|
177
186
|
// We delete expired records
|
|
178
187
|
// It is possible that they have different TTL, so we manage them individually
|
|
179
188
|
family.ips.splice(position, 1)
|
|
180
189
|
return this.pick(origin, hostnameRecords, affinity)
|
|
181
190
|
}
|
|
182
191
|
|
|
183
|
-
ip.timestamp = timestamp
|
|
184
|
-
|
|
185
|
-
this.lastIpFamily = newIpFamily
|
|
186
192
|
return ip
|
|
187
193
|
}
|
|
188
194
|
|
|
189
195
|
setRecords (origin, addresses) {
|
|
196
|
+
const timestamp = Date.now()
|
|
190
197
|
const records = { records: { 4: null, 6: null } }
|
|
191
198
|
for (const record of addresses) {
|
|
199
|
+
record.timestamp = timestamp
|
|
200
|
+
if (typeof record.ttl === 'number') {
|
|
201
|
+
// The record TTL is expected to be in ms
|
|
202
|
+
record.ttl = Math.min(record.ttl, this.#maxTTL)
|
|
203
|
+
} else {
|
|
204
|
+
record.ttl = this.#maxTTL
|
|
205
|
+
}
|
|
206
|
+
|
|
192
207
|
const familyRecords = records.records[record.family] ?? { ips: [] }
|
|
193
208
|
|
|
194
209
|
familyRecords.ips.push(record)
|
|
@@ -302,12 +317,20 @@ module.exports = interceptorOpts => {
|
|
|
302
317
|
throw new InvalidArgumentError('Invalid pick. Must be a function')
|
|
303
318
|
}
|
|
304
319
|
|
|
320
|
+
const dualStack = interceptorOpts?.dualStack ?? true
|
|
321
|
+
let affinity
|
|
322
|
+
if (dualStack) {
|
|
323
|
+
affinity = interceptorOpts?.affinity ?? null
|
|
324
|
+
} else {
|
|
325
|
+
affinity = interceptorOpts?.affinity ?? 4
|
|
326
|
+
}
|
|
327
|
+
|
|
305
328
|
const opts = {
|
|
306
329
|
maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
|
|
307
330
|
lookup: interceptorOpts?.lookup ?? null,
|
|
308
331
|
pick: interceptorOpts?.pick ?? null,
|
|
309
|
-
dualStack
|
|
310
|
-
affinity
|
|
332
|
+
dualStack,
|
|
333
|
+
affinity,
|
|
311
334
|
maxItems: interceptorOpts?.maxItems ?? Infinity
|
|
312
335
|
}
|
|
313
336
|
|