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.
@@ -20,10 +20,10 @@ import { createServer } from 'http'
20
20
  import proxy from 'proxy'
21
21
 
22
22
  const server = await buildServer()
23
- const proxy = await buildProxy()
23
+ const proxyServer = await buildProxy()
24
24
 
25
25
  const serverUrl = `http://localhost:${server.address().port}`
26
- const proxyUrl = `http://localhost:${proxy.address().port}`
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
- proxy.close()
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 proxy = await buildProxy()
76
+ const proxyServer = await buildProxy()
77
77
 
78
78
  const serverUrl = `http://localhost:${server.address().port}`
79
- const proxyUrl = `http://localhost:${proxy.address().port}`
79
+ const proxyUrl = `http://localhost:${proxyServer.address().port}`
80
80
 
81
- proxy.authenticate = function (req, fn) {
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
- proxy.close()
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
+
@@ -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[kAddClient](this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])))
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].splice(this[kClients].indexOf(dispatcher), 1)
104
- this[kClients].push(dispatcher)
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
- return dispatcher
181
+ this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]
182
+ this[kIndex] = maxWeightIndex
183
+ return this[kClients][maxWeightIndex]
107
184
  }
108
185
  }
109
186
 
@@ -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
 
@@ -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
 
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.7.0",
3
+ "version": "5.8.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": {