undici 5.6.1 → 5.8.1

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 CHANGED
@@ -176,7 +176,7 @@ Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
176
176
  * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
177
177
  * https://fetch.spec.whatwg.org/#fetch-method
178
178
 
179
- Only supported on Node 16.5+.
179
+ Only supported on Node 16.8+.
180
180
 
181
181
  This is [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) and is not yet fully compliant with the Fetch Standard.
182
182
  We plan to ship breaking changes to this feature until it is out of experimental.
@@ -465,7 +465,7 @@ agent.disableNetConnect()
465
465
  agent
466
466
  .get('https://example.com')
467
467
  .intercept({ method: 'GET', path: '/' })
468
- .reply(200, '')
468
+ .reply(200)
469
469
 
470
470
  const pendingInterceptors = agent.pendingInterceptors()
471
471
  // Returns [
@@ -508,7 +508,7 @@ agent.disableNetConnect()
508
508
  agent
509
509
  .get('https://example.com')
510
510
  .intercept({ method: 'GET', path: '/' })
511
- .reply(200, '')
511
+ .reply(200)
512
512
 
513
513
  agent.assertNoPendingInterceptors()
514
514
  // Throws an UndiciError with the following message:
@@ -1,6 +1,6 @@
1
1
  # Mocking Request
2
2
 
3
- Undici have its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP request and return mocked value instead. It can be useful for testing purposes.
3
+ Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
4
4
 
5
5
  Example:
6
6
 
@@ -8,7 +8,7 @@ Example:
8
8
  // bank.mjs
9
9
  import { request } from 'undici'
10
10
 
11
- export async function bankTransfer(recepient, amount) {
11
+ export async function bankTransfer(recipient, amount) {
12
12
  const { body } = await request('http://localhost:3000/bank-transfer',
13
13
  {
14
14
  method: 'POST',
@@ -16,7 +16,7 @@ export async function bankTransfer(recepient, amount) {
16
16
  'X-TOKEN-SECRET': 'SuperSecretToken',
17
17
  },
18
18
  body: JSON.stringify({
19
- recepient,
19
+ recipient,
20
20
  amount
21
21
  })
22
22
  }
@@ -48,7 +48,7 @@ mockPool.intercept({
48
48
  'X-TOKEN-SECRET': 'SuperSecretToken',
49
49
  },
50
50
  body: JSON.stringify({
51
- recepient: '1234567890',
51
+ recipient: '1234567890',
52
52
  amount: '100'
53
53
  })
54
54
  }).reply(200, {
@@ -77,7 +77,7 @@ Explore other MockAgent functionality [here](../api/MockAgent.md)
77
77
 
78
78
  ## Debug Mock Value
79
79
 
80
- When the interceptor we wrote are not the same undici will automatically call real HTTP request. To debug our mock value use `mockAgent.disableNetConnect()`
80
+ When the interceptor and the request options are not the same, undici will automatically make a real HTTP request. To prevent real requests from being made, use `mockAgent.disableNetConnect()`:
81
81
 
82
82
  ```js
83
83
  const mockAgent = new MockAgent();
@@ -89,7 +89,7 @@ mockAgent.disableNetConnect()
89
89
  const mockPool = mockAgent.get('http://localhost:3000');
90
90
 
91
91
  mockPool.intercept({
92
- path: '/bank-tanfer',
92
+ path: '/bank-transfer',
93
93
  method: 'POST',
94
94
  }).reply(200, {
95
95
  message: 'transaction processed'
@@ -103,7 +103,7 @@ const badRequest = await bankTransfer('1234567890', '100')
103
103
 
104
104
  ## Reply with data based on request
105
105
 
106
- If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`
106
+ If the mocked response needs to be dynamically derived from the request parameters, you can provide a function instead of an object to `reply`:
107
107
 
108
108
  ```js
109
109
  mockPool.intercept({
@@ -113,7 +113,7 @@ mockPool.intercept({
113
113
  'X-TOKEN-SECRET': 'SuperSecretToken',
114
114
  },
115
115
  body: JSON.stringify({
116
- recepient: '1234567890',
116
+ recipient: '1234567890',
117
117
  amount: '100'
118
118
  })
119
119
  }).reply(200, (opts) => {
@@ -129,7 +129,7 @@ in this case opts will be
129
129
  {
130
130
  method: 'POST',
131
131
  headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
132
- body: '{"recepient":"1234567890","amount":"100"}',
132
+ body: '{"recipient":"1234567890","amount":"100"}',
133
133
  origin: 'http://localhost:3000',
134
134
  path: '/bank-transfer'
135
135
  }
@@ -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
+
package/index.js CHANGED
@@ -80,7 +80,7 @@ function makeDispatcher (fn) {
80
80
  module.exports.setGlobalDispatcher = setGlobalDispatcher
81
81
  module.exports.getGlobalDispatcher = getGlobalDispatcher
82
82
 
83
- if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 5)) {
83
+ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
84
84
  let fetchImpl = null
85
85
  module.exports.fetch = async function fetch (resource) {
86
86
  if (!fetchImpl) {
@@ -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
 
@@ -75,14 +75,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
75
75
  })
76
76
  }
77
77
 
78
- const timeoutId = timeout
79
- ? setTimeout(onConnectTimeout, timeout, socket)
80
- : null
78
+ const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
81
79
 
82
80
  socket
83
81
  .setNoDelay(true)
84
82
  .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
85
- clearTimeout(timeoutId)
83
+ cancelTimeout()
86
84
 
87
85
  if (callback) {
88
86
  const cb = callback
@@ -91,7 +89,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
91
89
  }
92
90
  })
93
91
  .on('error', function (err) {
94
- clearTimeout(timeoutId)
92
+ cancelTimeout()
95
93
 
96
94
  if (callback) {
97
95
  const cb = callback
@@ -104,6 +102,31 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
104
102
  }
105
103
  }
106
104
 
105
+ function setupTimeout (onConnectTimeout, timeout) {
106
+ if (!timeout) {
107
+ return () => {}
108
+ }
109
+
110
+ let s1 = null
111
+ let s2 = null
112
+ const timeoutId = setTimeout(() => {
113
+ // setImmediate is added to make sure that we priotorise socket error events over timeouts
114
+ s1 = setImmediate(() => {
115
+ if (process.platform === 'win32') {
116
+ // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
117
+ s2 = setImmediate(() => onConnectTimeout())
118
+ } else {
119
+ onConnectTimeout()
120
+ }
121
+ })
122
+ }, timeout)
123
+ return () => {
124
+ clearTimeout(timeoutId)
125
+ clearImmediate(s1)
126
+ clearImmediate(s2)
127
+ }
128
+ }
129
+
107
130
  function onConnectTimeout (socket) {
108
131
  util.destroy(socket, new ConnectTimeoutError())
109
132
  }
@@ -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') {
@@ -140,8 +165,8 @@ class Request {
140
165
  }
141
166
 
142
167
  if (util.isFormDataLike(this.body)) {
143
- if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
144
- throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
168
+ if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
169
+ throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
145
170
  }
146
171
 
147
172
  if (!extractBody) {
@@ -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
@@ -15,12 +15,7 @@ const { isUint8Array, isArrayBuffer } = require('util/types')
15
15
  let ReadableStream
16
16
 
17
17
  async function * blobGen (blob) {
18
- if (blob.stream) {
19
- yield * blob.stream()
20
- } else {
21
- // istanbul ignore next: node < 16.7
22
- yield await blob.arrayBuffer()
23
- }
18
+ yield * blob.stream()
24
19
  }
25
20
 
26
21
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
@@ -263,6 +258,29 @@ function cloneBody (body) {
263
258
  }
264
259
  }
265
260
 
261
+ async function * consumeBody (body) {
262
+ if (body) {
263
+ if (isUint8Array(body)) {
264
+ yield body
265
+ } else {
266
+ const stream = body.stream
267
+
268
+ if (util.isDisturbed(stream)) {
269
+ throw new TypeError('disturbed')
270
+ }
271
+
272
+ if (stream.locked) {
273
+ throw new TypeError('locked')
274
+ }
275
+
276
+ // Compat.
277
+ stream[kBodyUsed] = true
278
+
279
+ yield * stream
280
+ }
281
+ }
282
+ }
283
+
266
284
  function bodyMixinMethods (instance) {
267
285
  const methods = {
268
286
  async blob () {
@@ -272,27 +290,14 @@ function bodyMixinMethods (instance) {
272
290
 
273
291
  const chunks = []
274
292
 
275
- if (this[kState].body) {
276
- if (isUint8Array(this[kState].body)) {
277
- chunks.push(this[kState].body)
278
- } else {
279
- const stream = this[kState].body.stream
280
-
281
- if (util.isDisturbed(stream)) {
282
- throw new TypeError('disturbed')
283
- }
284
-
285
- if (stream.locked) {
286
- throw new TypeError('locked')
287
- }
288
-
289
- // Compat.
290
- stream[kBodyUsed] = true
291
-
292
- for await (const chunk of stream) {
293
- chunks.push(chunk)
294
- }
293
+ for await (const chunk of consumeBody(this[kState].body)) {
294
+ if (!isUint8Array(chunk)) {
295
+ throw new TypeError('Expected Uint8Array chunk')
295
296
  }
297
+
298
+ // Assemble one final large blob with Uint8Array's can exhaust memory.
299
+ // That's why we create create multiple blob's and using references
300
+ chunks.push(new Blob([chunk]))
296
301
  }
297
302
 
298
303
  return new Blob(chunks, { type: this.headers.get('Content-Type') || '' })
@@ -303,8 +308,54 @@ function bodyMixinMethods (instance) {
303
308
  throw new TypeError('Illegal invocation')
304
309
  }
305
310
 
306
- const blob = await this.blob()
307
- return await blob.arrayBuffer()
311
+ const contentLength = this.headers.get('content-length')
312
+ const encoded = this.headers.has('content-encoding')
313
+
314
+ // if we have content length and no encoding, then we can
315
+ // pre allocate the buffer and just read the data into it
316
+ if (!encoded && contentLength) {
317
+ const buffer = new Uint8Array(contentLength)
318
+ let offset = 0
319
+
320
+ for await (const chunk of consumeBody(this[kState].body)) {
321
+ if (!isUint8Array(chunk)) {
322
+ throw new TypeError('Expected Uint8Array chunk')
323
+ }
324
+
325
+ buffer.set(chunk, offset)
326
+ offset += chunk.length
327
+ }
328
+
329
+ return buffer.buffer
330
+ }
331
+
332
+ // if we don't have content length, then we have to allocate 2x the
333
+ // size of the body, once for consumed data, and once for the final buffer
334
+
335
+ // This could be optimized by using growable ArrayBuffer, but it's not
336
+ // implemented yet. https://github.com/tc39/proposal-resizablearraybuffer
337
+
338
+ const chunks = []
339
+ let size = 0
340
+
341
+ for await (const chunk of consumeBody(this[kState].body)) {
342
+ if (!isUint8Array(chunk)) {
343
+ throw new TypeError('Expected Uint8Array chunk')
344
+ }
345
+
346
+ chunks.push(chunk)
347
+ size += chunk.byteLength
348
+ }
349
+
350
+ const buffer = new Uint8Array(size)
351
+ let offset = 0
352
+
353
+ for (const chunk of chunks) {
354
+ buffer.set(chunk, offset)
355
+ offset += chunk.byteLength
356
+ }
357
+
358
+ return buffer.buffer
308
359
  },
309
360
 
310
361
  async text () {
@@ -312,8 +363,21 @@ function bodyMixinMethods (instance) {
312
363
  throw new TypeError('Illegal invocation')
313
364
  }
314
365
 
315
- const blob = await this.blob()
316
- return toUSVString(await blob.text())
366
+ let result = ''
367
+ const textDecoder = new TextDecoder()
368
+
369
+ for await (const chunk of consumeBody(this[kState].body)) {
370
+ if (!isUint8Array(chunk)) {
371
+ throw new TypeError('Expected Uint8Array chunk')
372
+ }
373
+
374
+ result += textDecoder.decode(chunk, { stream: true })
375
+ }
376
+
377
+ // flush
378
+ result += textDecoder.decode()
379
+
380
+ return result
317
381
  },
318
382
 
319
383
  async json () {
@@ -340,7 +404,18 @@ function bodyMixinMethods (instance) {
340
404
  // 1. Let entries be the result of parsing bytes.
341
405
  let entries
342
406
  try {
343
- entries = new URLSearchParams(await this.text())
407
+ let text = ''
408
+ // application/x-www-form-urlencoded parser will keep the BOM.
409
+ // https://url.spec.whatwg.org/#concept-urlencoded-parser
410
+ const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
411
+ for await (const chunk of consumeBody(this[kState].body)) {
412
+ if (!isUint8Array(chunk)) {
413
+ throw new TypeError('Expected Uint8Array chunk')
414
+ }
415
+ text += textDecoder.decode(chunk, { stream: true })
416
+ }
417
+ text += textDecoder.decode()
418
+ entries = new URLSearchParams(text)
344
419
  } catch (err) {
345
420
  // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
346
421
  // 2. If entries is failure, then throw a TypeError.
@@ -63,7 +63,7 @@ const subresource = [
63
63
  /** @type {globalThis['DOMException']} */
64
64
  const DOMException = globalThis.DOMException ?? (() => {
65
65
  // DOMException was only made a global in Node v17.0.0,
66
- // but fetch supports >= v16.5.
66
+ // but fetch supports >= v16.8.
67
67
  try {
68
68
  atob('~')
69
69
  } catch (err) {
@@ -255,7 +255,7 @@ function percentDecode (input) {
255
255
  }
256
256
 
257
257
  // 3. Return output.
258
- return Uint8Array.of(...output)
258
+ return Uint8Array.from(output)
259
259
  }
260
260
 
261
261
  // https://mimesniff.spec.whatwg.org/#parse-a-mime-type
@@ -110,6 +110,7 @@ class HeadersList {
110
110
  // https://fetch.spec.whatwg.org/#concept-header-list-set
111
111
  set (name, value) {
112
112
  this[kHeadersSortedMap] = null
113
+ name = name.toLowerCase()
113
114
 
114
115
  // 1. If list contains name, then set the value of
115
116
  // the first such header to value and remove the
@@ -33,7 +33,8 @@ const {
33
33
  isBlobLike,
34
34
  sameOrigin,
35
35
  isCancelled,
36
- isAborted
36
+ isAborted,
37
+ isErrorLike
37
38
  } = require('./util')
38
39
  const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
39
40
  const assert = require('assert')
@@ -1854,7 +1855,7 @@ async function httpNetworkFetch (
1854
1855
  timingInfo.decodedBodySize += bytes?.byteLength ?? 0
1855
1856
 
1856
1857
  // 6. If bytes is failure, then terminate fetchParams’s controller.
1857
- if (bytes instanceof Error) {
1858
+ if (isErrorLike(bytes)) {
1858
1859
  fetchParams.controller.terminate(bytes)
1859
1860
  return
1860
1861
  }
@@ -1894,7 +1895,7 @@ async function httpNetworkFetch (
1894
1895
  // 3. Otherwise, if stream is readable, error stream with a TypeError.
1895
1896
  if (isReadable(stream)) {
1896
1897
  fetchParams.controller.controller.error(new TypeError('terminated', {
1897
- cause: reason instanceof Error ? reason : undefined
1898
+ cause: isErrorLike(reason) ? reason : undefined
1898
1899
  }))
1899
1900
  }
1900
1901
  }
@@ -1942,14 +1943,17 @@ async function httpNetworkFetch (
1942
1943
  }
1943
1944
 
1944
1945
  let codings = []
1946
+ let location = ''
1945
1947
 
1946
1948
  const headers = new Headers()
1947
1949
  for (let n = 0; n < headersList.length; n += 2) {
1948
- const key = headersList[n + 0].toString()
1949
- const val = headersList[n + 1].toString()
1950
+ const key = headersList[n + 0].toString('latin1')
1951
+ const val = headersList[n + 1].toString('latin1')
1950
1952
 
1951
1953
  if (key.toLowerCase() === 'content-encoding') {
1952
1954
  codings = val.split(',').map((x) => x.trim())
1955
+ } else if (key.toLowerCase() === 'location') {
1956
+ location = val
1953
1957
  }
1954
1958
 
1955
1959
  headers.append(key, val)
@@ -1960,7 +1964,7 @@ async function httpNetworkFetch (
1960
1964
  const decoders = []
1961
1965
 
1962
1966
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
1963
- if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status)) {
1967
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !(request.redirect === 'follow' && location)) {
1964
1968
  for (const coding of codings) {
1965
1969
  if (/(x-)?gzip/.test(coding)) {
1966
1970
  decoders.push(zlib.createGunzip())
@@ -1980,7 +1984,7 @@ async function httpNetworkFetch (
1980
1984
  statusText,
1981
1985
  headersList: headers[kHeadersList],
1982
1986
  body: decoders.length
1983
- ? pipeline(this.body, ...decoders, () => {})
1987
+ ? pipeline(this.body, ...decoders, () => { })
1984
1988
  : this.body.on('error', () => {})
1985
1989
  })
1986
1990