undici 5.7.0 → 5.8.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/docs/best-practices/proxy.md +8 -7
- package/lib/balanced-pool.js +81 -4
- package/lib/core/request.js +29 -0
- package/lib/fetch/body.js +16 -0
- package/lib/handler/redirect.js +2 -1
- package/lib/mock/mock-utils.js +2 -1
- package/package.json +1 -1
|
@@ -20,10 +20,10 @@ import { createServer } from 'http'
|
|
|
20
20
|
import proxy from 'proxy'
|
|
21
21
|
|
|
22
22
|
const server = await buildServer()
|
|
23
|
-
const
|
|
23
|
+
const proxyServer = await buildProxy()
|
|
24
24
|
|
|
25
25
|
const serverUrl = `http://localhost:${server.address().port}`
|
|
26
|
-
const proxyUrl = `http://localhost:${
|
|
26
|
+
const proxyUrl = `http://localhost:${proxyServer.address().port}`
|
|
27
27
|
|
|
28
28
|
server.on('request', (req, res) => {
|
|
29
29
|
console.log(req.url) // '/hello?foo=bar'
|
|
@@ -47,7 +47,7 @@ console.log(response.statusCode) // 200
|
|
|
47
47
|
console.log(JSON.parse(data)) // { hello: 'world' }
|
|
48
48
|
|
|
49
49
|
server.close()
|
|
50
|
-
|
|
50
|
+
proxyServer.close()
|
|
51
51
|
client.close()
|
|
52
52
|
|
|
53
53
|
function buildServer () {
|
|
@@ -73,12 +73,12 @@ import { createServer } from 'http'
|
|
|
73
73
|
import proxy from 'proxy'
|
|
74
74
|
|
|
75
75
|
const server = await buildServer()
|
|
76
|
-
const
|
|
76
|
+
const proxyServer = await buildProxy()
|
|
77
77
|
|
|
78
78
|
const serverUrl = `http://localhost:${server.address().port}`
|
|
79
|
-
const proxyUrl = `http://localhost:${
|
|
79
|
+
const proxyUrl = `http://localhost:${proxyServer.address().port}`
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
proxyServer.authenticate = function (req, fn) {
|
|
82
82
|
fn(null, req.headers['proxy-authorization'] === `Basic ${Buffer.from('user:pass').toString('base64')}`)
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -107,7 +107,7 @@ console.log(response.statusCode) // 200
|
|
|
107
107
|
console.log(JSON.parse(data)) // { hello: 'world' }
|
|
108
108
|
|
|
109
109
|
server.close()
|
|
110
|
-
|
|
110
|
+
proxyServer.close()
|
|
111
111
|
client.close()
|
|
112
112
|
|
|
113
113
|
function buildServer () {
|
|
@@ -124,3 +124,4 @@ function buildProxy () {
|
|
|
124
124
|
})
|
|
125
125
|
}
|
|
126
126
|
```
|
|
127
|
+
|
package/lib/balanced-pool.js
CHANGED
|
@@ -18,6 +18,17 @@ const { parseOrigin } = require('./core/util')
|
|
|
18
18
|
const kFactory = Symbol('factory')
|
|
19
19
|
|
|
20
20
|
const kOptions = Symbol('options')
|
|
21
|
+
const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor')
|
|
22
|
+
const kCurrentWeight = Symbol('kCurrentWeight')
|
|
23
|
+
const kIndex = Symbol('kIndex')
|
|
24
|
+
const kWeight = Symbol('kWeight')
|
|
25
|
+
const kMaxWeightPerServer = Symbol('kMaxWeightPerServer')
|
|
26
|
+
const kErrorPenalty = Symbol('kErrorPenalty')
|
|
27
|
+
|
|
28
|
+
function getGreatestCommonDivisor (a, b) {
|
|
29
|
+
if (b === 0) return a
|
|
30
|
+
return getGreatestCommonDivisor(b, a % b)
|
|
31
|
+
}
|
|
21
32
|
|
|
22
33
|
function defaultFactory (origin, opts) {
|
|
23
34
|
return new Pool(origin, opts)
|
|
@@ -28,6 +39,11 @@ class BalancedPool extends PoolBase {
|
|
|
28
39
|
super()
|
|
29
40
|
|
|
30
41
|
this[kOptions] = opts
|
|
42
|
+
this[kIndex] = -1
|
|
43
|
+
this[kCurrentWeight] = 0
|
|
44
|
+
|
|
45
|
+
this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100
|
|
46
|
+
this[kErrorPenalty] = this[kOptions].errorPenalty || 15
|
|
31
47
|
|
|
32
48
|
if (!Array.isArray(upstreams)) {
|
|
33
49
|
upstreams = [upstreams]
|
|
@@ -42,6 +58,7 @@ class BalancedPool extends PoolBase {
|
|
|
42
58
|
for (const upstream of upstreams) {
|
|
43
59
|
this.addUpstream(upstream)
|
|
44
60
|
}
|
|
61
|
+
this._updateBalancedPoolStats()
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
addUpstream (upstream) {
|
|
@@ -54,12 +71,40 @@ class BalancedPool extends PoolBase {
|
|
|
54
71
|
))) {
|
|
55
72
|
return this
|
|
56
73
|
}
|
|
74
|
+
const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions]))
|
|
75
|
+
|
|
76
|
+
this[kAddClient](pool)
|
|
77
|
+
pool.on('connect', () => {
|
|
78
|
+
pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
pool.on('connectionError', () => {
|
|
82
|
+
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
|
83
|
+
this._updateBalancedPoolStats()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
pool.on('disconnect', (...args) => {
|
|
87
|
+
const err = args[2]
|
|
88
|
+
if (err && err.code === 'UND_ERR_SOCKET') {
|
|
89
|
+
// decrease the weight of the pool.
|
|
90
|
+
pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty])
|
|
91
|
+
this._updateBalancedPoolStats()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
for (const client of this[kClients]) {
|
|
96
|
+
client[kWeight] = this[kMaxWeightPerServer]
|
|
97
|
+
}
|
|
57
98
|
|
|
58
|
-
this
|
|
99
|
+
this._updateBalancedPoolStats()
|
|
59
100
|
|
|
60
101
|
return this
|
|
61
102
|
}
|
|
62
103
|
|
|
104
|
+
_updateBalancedPoolStats () {
|
|
105
|
+
this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0)
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
removeUpstream (upstream) {
|
|
64
109
|
const upstreamOrigin = parseOrigin(upstream).origin
|
|
65
110
|
|
|
@@ -100,10 +145,42 @@ class BalancedPool extends PoolBase {
|
|
|
100
145
|
return
|
|
101
146
|
}
|
|
102
147
|
|
|
103
|
-
this[kClients].
|
|
104
|
-
|
|
148
|
+
const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true)
|
|
149
|
+
|
|
150
|
+
if (allClientsBusy) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let counter = 0
|
|
155
|
+
|
|
156
|
+
let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain])
|
|
157
|
+
|
|
158
|
+
while (counter++ < this[kClients].length) {
|
|
159
|
+
this[kIndex] = (this[kIndex] + 1) % this[kClients].length
|
|
160
|
+
const pool = this[kClients][this[kIndex]]
|
|
161
|
+
|
|
162
|
+
// find pool index with the largest weight
|
|
163
|
+
if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) {
|
|
164
|
+
maxWeightIndex = this[kIndex]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// decrease the current weight every `this[kClients].length`.
|
|
168
|
+
if (this[kIndex] === 0) {
|
|
169
|
+
// Set the current weight to the next lower weight.
|
|
170
|
+
this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]
|
|
171
|
+
|
|
172
|
+
if (this[kCurrentWeight] <= 0) {
|
|
173
|
+
this[kCurrentWeight] = this[kMaxWeightPerServer]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) {
|
|
177
|
+
return pool
|
|
178
|
+
}
|
|
179
|
+
}
|
|
105
180
|
|
|
106
|
-
|
|
181
|
+
this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
|
|
182
|
+
this[kIndex] = maxWeightIndex
|
|
183
|
+
return this[kClients][maxWeightIndex]
|
|
107
184
|
}
|
|
108
185
|
}
|
|
109
186
|
|
package/lib/core/request.js
CHANGED
|
@@ -7,6 +7,27 @@ const {
|
|
|
7
7
|
const assert = require('assert')
|
|
8
8
|
const util = require('./util')
|
|
9
9
|
|
|
10
|
+
// tokenRegExp and headerCharRegex have been lifted from
|
|
11
|
+
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verifies that the given val is a valid HTTP token
|
|
15
|
+
* per the rules defined in RFC 7230
|
|
16
|
+
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
17
|
+
*/
|
|
18
|
+
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Matches if val contains an invalid field-vchar
|
|
22
|
+
* field-value = *( field-content / obs-fold )
|
|
23
|
+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
|
24
|
+
* field-vchar = VCHAR / obs-text
|
|
25
|
+
*/
|
|
26
|
+
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
|
|
27
|
+
|
|
28
|
+
// Verifies that a given path is valid does not contain control chars \x00 to \x20
|
|
29
|
+
const invalidPathRegex = /[^\u0021-\u00ff]/
|
|
30
|
+
|
|
10
31
|
const kHandler = Symbol('handler')
|
|
11
32
|
|
|
12
33
|
const channels = {}
|
|
@@ -54,10 +75,14 @@ class Request {
|
|
|
54
75
|
method !== 'CONNECT'
|
|
55
76
|
) {
|
|
56
77
|
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
|
|
78
|
+
} else if (invalidPathRegex.exec(path) !== null) {
|
|
79
|
+
throw new InvalidArgumentError('invalid request path')
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
if (typeof method !== 'string') {
|
|
60
83
|
throw new InvalidArgumentError('method must be a string')
|
|
84
|
+
} else if (tokenRegExp.exec(method) === null) {
|
|
85
|
+
throw new InvalidArgumentError('invalid request method')
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
if (upgrade && typeof upgrade !== 'string') {
|
|
@@ -301,6 +326,10 @@ function processHeader (request, key, val) {
|
|
|
301
326
|
key.toLowerCase() === 'expect'
|
|
302
327
|
) {
|
|
303
328
|
throw new NotSupportedError('expect header not supported')
|
|
329
|
+
} else if (tokenRegExp.exec(key) === null) {
|
|
330
|
+
throw new InvalidArgumentError('invalid header key')
|
|
331
|
+
} else if (headerCharRegex.exec(val) !== null) {
|
|
332
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
304
333
|
} else {
|
|
305
334
|
request.headers += `${key}: ${val}\r\n`
|
|
306
335
|
}
|
package/lib/fetch/body.js
CHANGED
|
@@ -291,6 +291,10 @@ function bodyMixinMethods (instance) {
|
|
|
291
291
|
const chunks = []
|
|
292
292
|
|
|
293
293
|
for await (const chunk of consumeBody(this[kState].body)) {
|
|
294
|
+
if (!isUint8Array(chunk)) {
|
|
295
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
296
|
+
}
|
|
297
|
+
|
|
294
298
|
// Assemble one final large blob with Uint8Array's can exhaust memory.
|
|
295
299
|
// That's why we create create multiple blob's and using references
|
|
296
300
|
chunks.push(new Blob([chunk]))
|
|
@@ -314,6 +318,10 @@ function bodyMixinMethods (instance) {
|
|
|
314
318
|
let offset = 0
|
|
315
319
|
|
|
316
320
|
for await (const chunk of consumeBody(this[kState].body)) {
|
|
321
|
+
if (!isUint8Array(chunk)) {
|
|
322
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
323
|
+
}
|
|
324
|
+
|
|
317
325
|
buffer.set(chunk, offset)
|
|
318
326
|
offset += chunk.length
|
|
319
327
|
}
|
|
@@ -331,6 +339,10 @@ function bodyMixinMethods (instance) {
|
|
|
331
339
|
let size = 0
|
|
332
340
|
|
|
333
341
|
for await (const chunk of consumeBody(this[kState].body)) {
|
|
342
|
+
if (!isUint8Array(chunk)) {
|
|
343
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
344
|
+
}
|
|
345
|
+
|
|
334
346
|
chunks.push(chunk)
|
|
335
347
|
size += chunk.byteLength
|
|
336
348
|
}
|
|
@@ -355,6 +367,10 @@ function bodyMixinMethods (instance) {
|
|
|
355
367
|
const textDecoder = new TextDecoder()
|
|
356
368
|
|
|
357
369
|
for await (const chunk of consumeBody(this[kState].body)) {
|
|
370
|
+
if (!isUint8Array(chunk)) {
|
|
371
|
+
throw new TypeError('Expected Uint8Array chunk')
|
|
372
|
+
}
|
|
373
|
+
|
|
358
374
|
result += textDecoder.decode(chunk, { stream: true })
|
|
359
375
|
}
|
|
360
376
|
|
package/lib/handler/redirect.js
CHANGED
|
@@ -186,7 +186,8 @@ function shouldRemoveHeader (header, removeContent, unknownOrigin) {
|
|
|
186
186
|
return (
|
|
187
187
|
(header.length === 4 && header.toString().toLowerCase() === 'host') ||
|
|
188
188
|
(removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
|
|
189
|
-
(unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization')
|
|
189
|
+
(unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
|
|
190
|
+
(unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
|
|
190
191
|
)
|
|
191
192
|
}
|
|
192
193
|
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
kOrigin,
|
|
9
9
|
kGetNetConnect
|
|
10
10
|
} = require('./mock-symbols')
|
|
11
|
-
const { buildURL } = require('../core/util')
|
|
11
|
+
const { buildURL, nop } = require('../core/util')
|
|
12
12
|
|
|
13
13
|
function matchValue (match, value) {
|
|
14
14
|
if (typeof match === 'string') {
|
|
@@ -288,6 +288,7 @@ function mockDispatch (opts, handler) {
|
|
|
288
288
|
const responseHeaders = generateKeyValues(headers)
|
|
289
289
|
const responseTrailers = generateKeyValues(trailers)
|
|
290
290
|
|
|
291
|
+
handler.abort = nop
|
|
291
292
|
handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
|
|
292
293
|
handler.onData(Buffer.from(responseData))
|
|
293
294
|
handler.onComplete(responseTrailers)
|