undici 5.3.0 → 5.5.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
@@ -185,13 +185,12 @@ Help us improve the test coverage by following instructions at [nodejs/undici/#9
185
185
  Basic usage example:
186
186
 
187
187
  ```js
188
- import {fetch} from 'undici';
188
+ import { fetch } from 'undici';
189
189
 
190
- async function fetchJson() {
191
- const res = await fetch('https://example.com')
192
- const json = await res.json()
193
- console.log(json);
194
- }
190
+
191
+ const res = await fetch('https://example.com')
192
+ const json = await res.json()
193
+ console.log(json);
195
194
  ```
196
195
 
197
196
  You can pass an optional dispatcher to `fetch` as:
@@ -235,9 +234,7 @@ const data = {
235
234
  },
236
235
  };
237
236
 
238
- (async () => {
239
- await fetch("https://example.com", { body: data, method: 'POST' });
240
- })();
237
+ await fetch("https://example.com", { body: data, method: 'POST' });
241
238
  ```
242
239
 
243
240
  #### `response.body`
@@ -245,14 +242,12 @@ const data = {
245
242
  Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
246
243
 
247
244
  ```js
248
- import {fetch} from 'undici';
249
- import {Readable} from 'node:stream';
245
+ import { fetch } from 'undici';
246
+ import { Readable } from 'node:stream';
250
247
 
251
- async function fetchStream() {
252
- const response = await fetch('https://example.com')
253
- const readableWebStream = response.body;
254
- const readableNodeStream = Readable.fromWeb(readableWebStream);
255
- }
248
+ const response = await fetch('https://example.com')
249
+ const readableWebStream = response.body;
250
+ const readableNodeStream = Readable.fromWeb(readableWebStream);
256
251
  ```
257
252
 
258
253
  #### Specification Compliance
@@ -288,6 +283,15 @@ const headers = await fetch(url)
288
283
  .then(res => res.headers)
289
284
  ```
290
285
 
286
+ ##### Forbidden and Safelisted Header Names
287
+
288
+ * https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
289
+ * https://fetch.spec.whatwg.org/#forbidden-header-name
290
+ * https://fetch.spec.whatwg.org/#forbidden-response-header-name
291
+ * https://github.com/wintercg/fetch/issues/6
292
+
293
+ The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
294
+
291
295
  ### `undici.upgrade([url, options]): Promise`
292
296
 
293
297
  Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
@@ -57,6 +57,7 @@ Returns: `MockInterceptor` corresponding to the input options.
57
57
  * **method** `string | RegExp | (method: string) => boolean` - a matcher for the HTTP request method.
58
58
  * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
59
59
  * **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
60
+ * **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params.
60
61
 
61
62
  ### Return: `MockInterceptor`
62
63
 
@@ -15,7 +15,7 @@ class ConnectHandler extends AsyncResource {
15
15
  throw new InvalidArgumentError('invalid callback')
16
16
  }
17
17
 
18
- const { signal, opaque, responseHeaders } = opts
18
+ const { signal, opaque, responseHeaders, httpTunnel } = opts
19
19
 
20
20
  if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') {
21
21
  throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget')
@@ -27,6 +27,7 @@ class ConnectHandler extends AsyncResource {
27
27
  this.responseHeaders = responseHeaders || null
28
28
  this.callback = callback
29
29
  this.abort = null
30
+ this.httpTunnel = httpTunnel
30
31
 
31
32
  addSignal(this, signal)
32
33
  }
@@ -40,8 +41,23 @@ class ConnectHandler extends AsyncResource {
40
41
  this.context = context
41
42
  }
42
43
 
43
- onHeaders () {
44
- throw new SocketError('bad connect', null)
44
+ onHeaders (statusCode) {
45
+ // when httpTunnel headers are allowed
46
+ if (this.httpTunnel) {
47
+ const { callback, opaque } = this
48
+ if (statusCode !== 200) {
49
+ if (callback) {
50
+ this.callback = null
51
+ const err = new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')
52
+ queueMicrotask(() => {
53
+ this.runInAsyncScope(callback, null, err, { opaque })
54
+ })
55
+ }
56
+ return 1
57
+ }
58
+ } else {
59
+ throw new SocketError('bad connect', null)
60
+ }
45
61
  }
46
62
 
47
63
  onUpgrade (statusCode, rawHeaders, socket) {
package/lib/client.js CHANGED
@@ -873,6 +873,11 @@ class Parser {
873
873
  // have been queued since then.
874
874
  util.destroy(socket, new InformationalError('reset'))
875
875
  return constants.ERROR.PAUSED
876
+ } else if (client[kPipelining] === 1) {
877
+ // We must wait a full event loop cycle to reuse this socket to make sure
878
+ // that non-spec compliant servers are not closing the connection even if they
879
+ // said they won't.
880
+ setImmediate(resume, client)
876
881
  } else {
877
882
  resume(client)
878
883
  }
@@ -21,7 +21,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
21
21
  timeout = timeout == null ? 10e3 : timeout
22
22
  maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions
23
23
 
24
- return function connect ({ hostname, host, protocol, port, servername }, callback) {
24
+ return function connect ({ hostname, host, protocol, port, servername, httpSocket }, callback) {
25
25
  let socket
26
26
  if (protocol === 'https:') {
27
27
  if (!tls) {
@@ -39,6 +39,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
39
39
  ...options,
40
40
  servername,
41
41
  session,
42
+ socket: httpSocket, // upgrade socket connection
42
43
  port: port || 443,
43
44
  host: hostname
44
45
  })
@@ -65,6 +66,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
65
66
  }
66
67
  })
67
68
  } else {
69
+ assert(!httpSocket, 'httpSocket can only be sent on TLS update')
68
70
  socket = net.connect({
69
71
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
70
72
  ...options,
@@ -48,7 +48,11 @@ class Request {
48
48
  }, handler) {
49
49
  if (typeof path !== 'string') {
50
50
  throw new InvalidArgumentError('path must be a string')
51
- } else if (path[0] !== '/' && !(path.startsWith('http://') || path.startsWith('https://'))) {
51
+ } else if (
52
+ path[0] !== '/' &&
53
+ !(path.startsWith('http://') || path.startsWith('https://')) &&
54
+ method !== 'CONNECT'
55
+ ) {
52
56
  throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
53
57
  }
54
58
 
@@ -80,13 +84,12 @@ class Request {
80
84
  this.body = null
81
85
  } else if (util.isStream(body)) {
82
86
  this.body = body
83
- } else if (body instanceof DataView) {
84
- // TODO: Why is DataView special?
85
- this.body = body.buffer.byteLength ? Buffer.from(body.buffer) : null
86
- } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
87
- this.body = body.byteLength ? Buffer.from(body) : null
88
87
  } else if (util.isBuffer(body)) {
89
88
  this.body = body.byteLength ? body : null
89
+ } else if (ArrayBuffer.isView(body)) {
90
+ this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
91
+ } else if (body instanceof ArrayBuffer) {
92
+ this.body = body.byteLength ? Buffer.from(body) : null
90
93
  } else if (typeof body === 'string') {
91
94
  this.body = body.length ? Buffer.from(body) : null
92
95
  } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
package/lib/fetch/body.js CHANGED
@@ -9,7 +9,7 @@ const { kBodyUsed } = require('../core/symbols')
9
9
  const assert = require('assert')
10
10
  const { NotSupportedError } = require('../core/errors')
11
11
  const { isErrored } = require('../core/util')
12
- const { isUint8Array } = require('util/types')
12
+ const { isUint8Array, isArrayBuffer } = require('util/types')
13
13
 
14
14
  let ReadableStream
15
15
 
@@ -61,7 +61,7 @@ function extractBody (object, keepalive = false) {
61
61
 
62
62
  // Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
63
63
  contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
64
- } else if (object instanceof ArrayBuffer || ArrayBuffer.isView(object)) {
64
+ } else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
65
65
  // BufferSource
66
66
 
67
67
  if (object instanceof DataView) {
@@ -1,28 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const forbiddenHeaderNames = [
4
- 'accept-charset',
5
- 'accept-encoding',
6
- 'access-control-request-headers',
7
- 'access-control-request-method',
8
- 'connection',
9
- 'content-length',
10
- 'cookie',
11
- 'cookie2',
12
- 'date',
13
- 'dnt',
14
- 'expect',
15
- 'host',
16
- 'keep-alive',
17
- 'origin',
18
- 'referer',
19
- 'te',
20
- 'trailer',
21
- 'transfer-encoding',
22
- 'upgrade',
23
- 'via'
24
- ]
25
-
26
3
  const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
27
4
 
28
5
  const nullBodyStatus = [101, 204, 205, 304]
@@ -58,9 +35,6 @@ const requestCache = [
58
35
  'only-if-cached'
59
36
  ]
60
37
 
61
- // https://fetch.spec.whatwg.org/#forbidden-response-header-name
62
- const forbiddenResponseHeaderNames = ['set-cookie', 'set-cookie2']
63
-
64
38
  const requestBodyHeader = [
65
39
  'content-encoding',
66
40
  'content-language',
@@ -86,12 +60,8 @@ const subresource = [
86
60
  ''
87
61
  ]
88
62
 
89
- const corsSafeListedResponseHeaderNames = [] // TODO
90
-
91
63
  module.exports = {
92
64
  subresource,
93
- forbiddenResponseHeaderNames,
94
- corsSafeListedResponseHeaderNames,
95
65
  forbiddenMethods,
96
66
  requestBodyHeader,
97
67
  referrerPolicy,
@@ -99,7 +69,6 @@ module.exports = {
99
69
  requestMode,
100
70
  requestCredentials,
101
71
  requestCache,
102
- forbiddenHeaderNames,
103
72
  redirectStatus,
104
73
  corsSafeListedMethods,
105
74
  nullBodyStatus,
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { isBlobLike, isFileLike, toUSVString } = require('./util')
3
+ const { isBlobLike, isFileLike, toUSVString, makeIterator } = require('./util')
4
4
  const { kState } = require('./symbols')
5
5
  const { File, FileLike } = require('./file')
6
6
  const { Blob } = require('buffer')
@@ -187,45 +187,68 @@ class FormData {
187
187
  return this.constructor.name
188
188
  }
189
189
 
190
- * entries () {
190
+ entries () {
191
191
  if (!(this instanceof FormData)) {
192
192
  throw new TypeError('Illegal invocation')
193
193
  }
194
194
 
195
- for (const pair of this) {
196
- yield pair
197
- }
195
+ return makeIterator(
196
+ makeIterable(this[kState], 'entries'),
197
+ 'FormData'
198
+ )
198
199
  }
199
200
 
200
- * keys () {
201
+ keys () {
201
202
  if (!(this instanceof FormData)) {
202
203
  throw new TypeError('Illegal invocation')
203
204
  }
204
205
 
205
- for (const [key] of this) {
206
- yield key
206
+ return makeIterator(
207
+ makeIterable(this[kState], 'keys'),
208
+ 'FormData'
209
+ )
210
+ }
211
+
212
+ values () {
213
+ if (!(this instanceof FormData)) {
214
+ throw new TypeError('Illegal invocation')
207
215
  }
216
+
217
+ return makeIterator(
218
+ makeIterable(this[kState], 'values'),
219
+ 'FormData'
220
+ )
208
221
  }
209
222
 
210
- * values () {
223
+ /**
224
+ * @param {(value: string, key: string, self: FormData) => void} callbackFn
225
+ * @param {unknown} thisArg
226
+ */
227
+ forEach (callbackFn, thisArg = globalThis) {
211
228
  if (!(this instanceof FormData)) {
212
229
  throw new TypeError('Illegal invocation')
213
230
  }
214
231
 
215
- for (const [, value] of this) {
216
- yield value
232
+ if (arguments.length < 1) {
233
+ throw new TypeError(
234
+ `Failed to execute 'forEach' on 'FormData': 1 argument required, but only ${arguments.length} present.`
235
+ )
217
236
  }
218
- }
219
237
 
220
- * [Symbol.iterator] () {
221
- // The value pairs to iterate over are this’s entry list’s entries with
222
- // the key being the name and the value being the value.
223
- for (const { name, value } of this[kState]) {
224
- yield [name, value]
238
+ if (typeof callbackFn !== 'function') {
239
+ throw new TypeError(
240
+ "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
241
+ )
242
+ }
243
+
244
+ for (const [key, value] of this) {
245
+ callbackFn.apply(thisArg, [value, key, this])
225
246
  }
226
247
  }
227
248
  }
228
249
 
250
+ FormData.prototype[Symbol.iterator] = FormData.prototype.entries
251
+
229
252
  function makeEntry (name, value, filename) {
230
253
  // To create an entry for name, value, and optionally a filename, run these
231
254
  // steps:
@@ -243,8 +266,8 @@ function makeEntry (name, value, filename) {
243
266
  // object, representing the same bytes, whose name attribute value is "blob".
244
267
  if (isBlobLike(value) && !isFileLike(value)) {
245
268
  value = value instanceof Blob
246
- ? new File([value], 'blob')
247
- : new FileLike(value, 'blob')
269
+ ? new File([value], 'blob', value)
270
+ : new FileLike(value, 'blob', value)
248
271
  }
249
272
 
250
273
  // 4. If value is (now) a File object and filename is given, then set value to a
@@ -256,8 +279,8 @@ function makeEntry (name, value, filename) {
256
279
  // creating one more File instance doesn't make much sense....
257
280
  if (isFileLike(value) && filename != null) {
258
281
  value = value instanceof File
259
- ? new File([value], filename)
260
- : new FileLike(value, filename)
282
+ ? new File([value], filename, value)
283
+ : new FileLike(value, filename, value)
261
284
  }
262
285
 
263
286
  // 5. Set entry’s value to value.
@@ -267,4 +290,18 @@ function makeEntry (name, value, filename) {
267
290
  return entry
268
291
  }
269
292
 
293
+ function * makeIterable (entries, type) {
294
+ // The value pairs to iterate over are this’s entry list’s entries
295
+ // with the key being the name and the value being the value.
296
+ for (const { name, value } of entries) {
297
+ if (type === 'entries') {
298
+ yield [name, value]
299
+ } else if (type === 'values') {
300
+ yield value
301
+ } else {
302
+ yield name
303
+ }
304
+ }
305
+ }
306
+
270
307
  module.exports = { FormData }
@@ -6,10 +6,7 @@ const { validateHeaderName, validateHeaderValue } = require('http')
6
6
  const { kHeadersList } = require('../core/symbols')
7
7
  const { kGuard } = require('./symbols')
8
8
  const { kEnumerableProperty } = require('../core/util')
9
- const {
10
- forbiddenHeaderNames,
11
- forbiddenResponseHeaderNames
12
- } = require('./constants')
9
+ const { makeIterator } = require('./util')
13
10
 
14
11
  const kHeadersMap = Symbol('headers map')
15
12
  const kHeadersSortedMap = Symbol('headers map sorted')
@@ -77,33 +74,6 @@ function fill (headers, object) {
77
74
  }
78
75
  }
79
76
 
80
- // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
81
- const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
82
-
83
- // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
84
- function makeHeadersIterator (iterator) {
85
- const i = {
86
- next () {
87
- if (Object.getPrototypeOf(this) !== i) {
88
- throw new TypeError(
89
- '\'next\' called on an object that does not implement interface Headers Iterator.'
90
- )
91
- }
92
-
93
- return iterator.next()
94
- },
95
- // The class string of an iterator prototype object for a given interface is the
96
- // result of concatenating the identifier of the interface and the string " Iterator".
97
- [Symbol.toStringTag]: 'Headers Iterator'
98
- }
99
-
100
- // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%.
101
- Object.setPrototypeOf(i, esIteratorPrototype)
102
- // esIteratorPrototype needs to be the prototype of i
103
- // which is the prototype of an empty object. Yes, it's confusing.
104
- return Object.setPrototypeOf({}, i)
105
- }
106
-
107
77
  class HeadersList {
108
78
  constructor (init) {
109
79
  if (init instanceof HeadersList) {
@@ -115,6 +85,11 @@ class HeadersList {
115
85
  }
116
86
  }
117
87
 
88
+ clear () {
89
+ this[kHeadersMap].clear()
90
+ this[kHeadersSortedMap] = null
91
+ }
92
+
118
93
  append (name, value) {
119
94
  this[kHeadersSortedMap] = null
120
95
 
@@ -211,22 +186,11 @@ class Headers {
211
186
  )
212
187
  }
213
188
 
214
- const normalizedName = normalizeAndValidateHeaderName(String(name))
215
-
189
+ // Note: undici does not implement forbidden header names
216
190
  if (this[kGuard] === 'immutable') {
217
191
  throw new TypeError('immutable')
218
- } else if (
219
- this[kGuard] === 'request' &&
220
- forbiddenHeaderNames.includes(normalizedName)
221
- ) {
222
- return
223
192
  } else if (this[kGuard] === 'request-no-cors') {
224
193
  // TODO
225
- } else if (
226
- this[kGuard] === 'response' &&
227
- forbiddenResponseHeaderNames.includes(normalizedName)
228
- ) {
229
- return
230
194
  }
231
195
 
232
196
  return this[kHeadersList].append(String(name), String(value))
@@ -244,22 +208,11 @@ class Headers {
244
208
  )
245
209
  }
246
210
 
247
- const normalizedName = normalizeAndValidateHeaderName(String(name))
248
-
211
+ // Note: undici does not implement forbidden header names
249
212
  if (this[kGuard] === 'immutable') {
250
213
  throw new TypeError('immutable')
251
- } else if (
252
- this[kGuard] === 'request' &&
253
- forbiddenHeaderNames.includes(normalizedName)
254
- ) {
255
- return
256
214
  } else if (this[kGuard] === 'request-no-cors') {
257
215
  // TODO
258
- } else if (
259
- this[kGuard] === 'response' &&
260
- forbiddenResponseHeaderNames.includes(normalizedName)
261
- ) {
262
- return
263
216
  }
264
217
 
265
218
  return this[kHeadersList].delete(String(name))
@@ -307,20 +260,11 @@ class Headers {
307
260
  )
308
261
  }
309
262
 
263
+ // Note: undici does not implement forbidden header names
310
264
  if (this[kGuard] === 'immutable') {
311
265
  throw new TypeError('immutable')
312
- } else if (
313
- this[kGuard] === 'request' &&
314
- forbiddenHeaderNames.includes(String(name).toLocaleLowerCase())
315
- ) {
316
- return
317
266
  } else if (this[kGuard] === 'request-no-cors') {
318
267
  // TODO
319
- } else if (
320
- this[kGuard] === 'response' &&
321
- forbiddenResponseHeaderNames.includes(String(name).toLocaleLowerCase())
322
- ) {
323
- return
324
268
  }
325
269
 
326
270
  return this[kHeadersList].set(String(name), String(value))
@@ -336,7 +280,7 @@ class Headers {
336
280
  throw new TypeError('Illegal invocation')
337
281
  }
338
282
 
339
- return makeHeadersIterator(this[kHeadersSortedMap].keys())
283
+ return makeIterator(this[kHeadersSortedMap].keys(), 'Headers')
340
284
  }
341
285
 
342
286
  values () {
@@ -344,7 +288,7 @@ class Headers {
344
288
  throw new TypeError('Illegal invocation')
345
289
  }
346
290
 
347
- return makeHeadersIterator(this[kHeadersSortedMap].values())
291
+ return makeIterator(this[kHeadersSortedMap].values(), 'Headers')
348
292
  }
349
293
 
350
294
  entries () {
@@ -352,7 +296,7 @@ class Headers {
352
296
  throw new TypeError('Illegal invocation')
353
297
  }
354
298
 
355
- return makeHeadersIterator(this[kHeadersSortedMap].entries())
299
+ return makeIterator(this[kHeadersSortedMap].entries(), 'Headers')
356
300
  }
357
301
 
358
302
  /**
@@ -1164,7 +1164,7 @@ async function httpRedirectFetch (fetchParams, response) {
1164
1164
  if (
1165
1165
  ([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
1166
1166
  (actualResponse.status === 303 &&
1167
- !['GET', 'HEADER'].includes(request.method))
1167
+ !['GET', 'HEAD'].includes(request.method))
1168
1168
  ) {
1169
1169
  // then:
1170
1170
  // 1. Set request’s method to `GET` and request’s body to null.
@@ -384,8 +384,8 @@ class Request {
384
384
  // Realm, whose header list is request’s header list and guard is
385
385
  // "request".
386
386
  this[kHeaders] = new Headers()
387
- this[kHeaders][kGuard] = 'request'
388
387
  this[kHeaders][kHeadersList] = request.headersList
388
+ this[kHeaders][kGuard] = 'request'
389
389
  this[kHeaders][kRealm] = this[kRealm]
390
390
 
391
391
  // 31. If this’s request’s mode is "no-cors", then:
@@ -406,7 +406,7 @@ class Request {
406
406
  if (Object.keys(init).length !== 0) {
407
407
  // 1. Let headers be a copy of this’s headers and its associated header
408
408
  // list.
409
- let headers = new Headers(this.headers)
409
+ let headers = new Headers(this[kHeaders])
410
410
 
411
411
  // 2. If init["headers"] exists, then set headers to init["headers"].
412
412
  if (init.headers !== undefined) {
@@ -414,18 +414,17 @@ class Request {
414
414
  }
415
415
 
416
416
  // 3. Empty this’s headers’s header list.
417
- this[kState].headersList = new HeadersList()
418
- this[kHeaders][kHeadersList] = this[kState].headersList
417
+ this[kHeaders][kHeadersList].clear()
419
418
 
420
419
  // 4. If headers is a Headers object, then for each header in its header
421
420
  // list, append header’s name/header’s value to this’s headers.
422
421
  if (headers.constructor.name === 'Headers') {
423
- for (const [key, val] of headers[kHeadersList] || headers) {
422
+ for (const [key, val] of headers) {
424
423
  this[kHeaders].append(key, val)
425
424
  }
426
425
  } else {
427
426
  // 5. Otherwise, fill this’s headers with headers.
428
- fillHeaders(this[kState].headersList, headers)
427
+ fillHeaders(this[kHeaders], headers)
429
428
  }
430
429
  }
431
430
 
@@ -8,9 +8,7 @@ const { kEnumerableProperty } = util
8
8
  const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require('./util')
9
9
  const {
10
10
  redirectStatus,
11
- nullBodyStatus,
12
- forbiddenResponseHeaderNames,
13
- corsSafeListedResponseHeaderNames
11
+ nullBodyStatus
14
12
  } = require('./constants')
15
13
  const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
16
14
  const { kHeadersList } = require('../core/symbols')
@@ -380,28 +378,6 @@ function makeFilteredResponse (response, state) {
380
378
  })
381
379
  }
382
380
 
383
- function makeFilteredHeadersList (headersList, filter) {
384
- return new Proxy(headersList, {
385
- get (target, prop) {
386
- // Override methods used by Headers class.
387
- if (prop === 'get' || prop === 'has') {
388
- const defaultReturn = prop === 'has' ? false : null
389
- return (name) => filter(name) ? target[prop](name) : defaultReturn
390
- } else if (prop === Symbol.iterator) {
391
- return function * () {
392
- for (const entry of target) {
393
- if (filter(entry[0])) {
394
- yield entry
395
- }
396
- }
397
- }
398
- } else {
399
- return target[prop]
400
- }
401
- }
402
- })
403
- }
404
-
405
381
  // https://fetch.spec.whatwg.org/#concept-filtered-response
406
382
  function filterResponse (response, type) {
407
383
  // Set response to the following filtered response with response as its
@@ -411,12 +387,10 @@ function filterResponse (response, type) {
411
387
  // and header list excludes any headers in internal response’s header list
412
388
  // whose name is a forbidden response-header name.
413
389
 
390
+ // Note: undici does not implement forbidden response-header names
414
391
  return makeFilteredResponse(response, {
415
392
  type: 'basic',
416
- headersList: makeFilteredHeadersList(
417
- response.headersList,
418
- (name) => !forbiddenResponseHeaderNames.includes(name.toLowerCase())
419
- )
393
+ headersList: response.headersList
420
394
  })
421
395
  } else if (type === 'cors') {
422
396
  // A CORS filtered response is a filtered response whose type is "cors"
@@ -424,9 +398,10 @@ function filterResponse (response, type) {
424
398
  // list whose name is not a CORS-safelisted response-header name, given
425
399
  // internal response’s CORS-exposed header-name list.
426
400
 
401
+ // Note: undici does not implement CORS-safelisted response-header names
427
402
  return makeFilteredResponse(response, {
428
403
  type: 'cors',
429
- headersList: makeFilteredHeadersList(response.headersList, (name) => !corsSafeListedResponseHeaderNames.includes(name))
404
+ headersList: response.headersList
430
405
  })
431
406
  } else if (type === 'opaque') {
432
407
  // An opaque filtered response is a filtered response whose type is
@@ -449,7 +424,7 @@ function filterResponse (response, type) {
449
424
  type: 'opaqueredirect',
450
425
  status: 0,
451
426
  statusText: '',
452
- headersList: makeFilteredHeadersList(response.headersList, () => false),
427
+ headersList: [],
453
428
  body: null
454
429
  })
455
430
  } else {
package/lib/fetch/util.js CHANGED
@@ -361,6 +361,33 @@ function serializeJavascriptValueToJSONString (value) {
361
361
  return result
362
362
  }
363
363
 
364
+ // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
365
+ const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
366
+
367
+ // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
368
+ function makeIterator (iterator, name) {
369
+ const i = {
370
+ next () {
371
+ if (Object.getPrototypeOf(this) !== i) {
372
+ throw new TypeError(
373
+ `'next' called on an object that does not implement interface ${name} Iterator.`
374
+ )
375
+ }
376
+
377
+ return iterator.next()
378
+ },
379
+ // The class string of an iterator prototype object for a given interface is the
380
+ // result of concatenating the identifier of the interface and the string " Iterator".
381
+ [Symbol.toStringTag]: `${name} Iterator`
382
+ }
383
+
384
+ // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%.
385
+ Object.setPrototypeOf(i, esIteratorPrototype)
386
+ // esIteratorPrototype needs to be the prototype of i
387
+ // which is the prototype of an empty object. Yes, it's confusing.
388
+ return Object.setPrototypeOf({}, i)
389
+ }
390
+
364
391
  module.exports = {
365
392
  isAborted,
366
393
  isCancelled,
@@ -390,5 +417,6 @@ module.exports = {
390
417
  isValidReasonPhrase,
391
418
  sameOrigin,
392
419
  normalizeMethod,
393
- serializeJavascriptValueToJSONString
420
+ serializeJavascriptValueToJSONString,
421
+ makeIterator
394
422
  }
@@ -10,6 +10,7 @@ const {
10
10
  kMockDispatch
11
11
  } = require('./mock-symbols')
12
12
  const { InvalidArgumentError } = require('../core/errors')
13
+ const { buildURL } = require('../core/util')
13
14
 
14
15
  /**
15
16
  * Defines the scope API for an interceptor reply
@@ -70,9 +71,13 @@ class MockInterceptor {
70
71
  // As per RFC 3986, clients are not supposed to send URI
71
72
  // fragments to servers when they retrieve a document,
72
73
  if (typeof opts.path === 'string') {
73
- // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
74
- const parsedURL = new URL(opts.path, 'data://')
75
- opts.path = parsedURL.pathname + parsedURL.search
74
+ if (opts.query) {
75
+ opts.path = buildURL(opts.path, opts.query)
76
+ } else {
77
+ // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811
78
+ const parsedURL = new URL(opts.path, 'data://')
79
+ opts.path = parsedURL.pathname + parsedURL.search
80
+ }
76
81
  }
77
82
  if (typeof opts.method === 'string') {
78
83
  opts.method = opts.method.toUpperCase()
@@ -8,6 +8,7 @@ const {
8
8
  kOrigin,
9
9
  kGetNetConnect
10
10
  } = require('./mock-symbols')
11
+ const { buildURL } = require('../core/util')
11
12
 
12
13
  function matchValue (match, value) {
13
14
  if (typeof match === 'string') {
@@ -98,10 +99,12 @@ function getResponseData (data) {
98
99
  }
99
100
 
100
101
  function getMockDispatch (mockDispatches, key) {
102
+ const resolvedPath = key.query ? buildURL(key.path, key.query) : key.path
103
+
101
104
  // Match path
102
- let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(path, key.path))
105
+ let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(path, resolvedPath))
103
106
  if (matchedMockDispatches.length === 0) {
104
- throw new MockNotMatchedError(`Mock dispatch not matched for path '${key.path}'`)
107
+ throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
105
108
  }
106
109
 
107
110
  // Match method
@@ -146,12 +149,13 @@ function deleteMockDispatch (mockDispatches, key) {
146
149
  }
147
150
 
148
151
  function buildKey (opts) {
149
- const { path, method, body, headers } = opts
152
+ const { path, method, body, headers, query } = opts
150
153
  return {
151
154
  path,
152
155
  method,
153
156
  body,
154
- headers
157
+ headers,
158
+ query
155
159
  }
156
160
  }
157
161
 
@@ -1,28 +1,46 @@
1
1
  'use strict'
2
2
 
3
3
  const { kProxy, kClose, kDestroy } = require('./core/symbols')
4
+ const Client = require('./agent')
4
5
  const Agent = require('./agent')
5
6
  const DispatcherBase = require('./dispatcher-base')
6
7
  const { InvalidArgumentError } = require('./core/errors')
8
+ const buildConnector = require('./core/connect')
7
9
 
8
10
  const kAgent = Symbol('proxy agent')
11
+ const kClient = Symbol('proxy client')
12
+ const kProxyHeaders = Symbol('proxy headers')
13
+ const kRequestTls = Symbol('request tls settings')
14
+ const kProxyTls = Symbol('proxy tls settings')
15
+ const kConnectEndpoint = Symbol('connect endpoint function')
9
16
 
10
17
  class ProxyAgent extends DispatcherBase {
11
18
  constructor (opts) {
12
19
  super(opts)
13
20
  this[kProxy] = buildProxyOptions(opts)
14
- this[kAgent] = new Agent(opts)
21
+ this[kRequestTls] = opts.requestTls
22
+ this[kProxyTls] = opts.proxyTls
23
+ this[kProxyHeaders] = {}
24
+
25
+ if (opts.auth) {
26
+ this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`
27
+ }
28
+
29
+ const connect = buildConnector({ ...opts.proxyTls })
30
+ this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
31
+ this[kClient] = new Client({ origin: opts.origin, connect })
32
+ this[kAgent] = new Agent({ ...opts, connect: this.connectTunnel.bind(this) })
15
33
  }
16
34
 
17
35
  dispatch (opts, handler) {
18
36
  const { host } = new URL(opts.origin)
37
+ const headers = buildHeaders(opts.headers)
38
+ throwIfProxyAuthIsSent(headers)
19
39
  return this[kAgent].dispatch(
20
40
  {
21
41
  ...opts,
22
- origin: this[kProxy].uri,
23
- path: opts.origin + opts.path,
24
42
  headers: {
25
- ...buildHeaders(opts.headers),
43
+ ...headers,
26
44
  host
27
45
  }
28
46
  },
@@ -30,12 +48,43 @@ class ProxyAgent extends DispatcherBase {
30
48
  )
31
49
  }
32
50
 
51
+ async connectTunnel (opts, callback) {
52
+ try {
53
+ const { socket } = await this[kClient].connect({
54
+ origin: this[kProxy].origin,
55
+ port: this[kProxy].port,
56
+ path: opts.host,
57
+ signal: opts.signal,
58
+ headers: {
59
+ ...this[kProxyHeaders],
60
+ host: opts.host
61
+ },
62
+ httpTunnel: true
63
+ })
64
+ if (opts.protocol !== 'https:') {
65
+ callback(null, socket)
66
+ return
67
+ }
68
+ let servername
69
+ if (this[kRequestTls]) {
70
+ servername = this[kRequestTls].servername
71
+ } else {
72
+ servername = opts.servername
73
+ }
74
+ this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
75
+ } catch (err) {
76
+ callback(err)
77
+ }
78
+ }
79
+
33
80
  async [kClose] () {
34
81
  await this[kAgent].close()
82
+ await this[kClient].close()
35
83
  }
36
84
 
37
85
  async [kDestroy] () {
38
86
  await this[kAgent].destroy()
87
+ await this[kClient].destroy()
39
88
  }
40
89
  }
41
90
 
@@ -48,10 +97,7 @@ function buildProxyOptions (opts) {
48
97
  throw new InvalidArgumentError('Proxy opts.uri is mandatory')
49
98
  }
50
99
 
51
- return {
52
- uri: opts.uri,
53
- protocol: opts.protocol || 'https'
54
- }
100
+ return new URL(opts.uri)
55
101
  }
56
102
 
57
103
  /**
@@ -75,4 +121,20 @@ function buildHeaders (headers) {
75
121
  return headers
76
122
  }
77
123
 
124
+ /**
125
+ * @param {Record<string, string>} headers
126
+ *
127
+ * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers
128
+ * Nevertheless, it was changed and to avoid a security vulnerability by end users
129
+ * this check was created.
130
+ * It should be removed in the next major version for performance reasons
131
+ */
132
+ function throwIfProxyAuthIsSent (headers) {
133
+ const existProxyAuth = headers && Object.keys(headers)
134
+ .find((key) => key.toLowerCase() === 'proxy-authorization')
135
+ if (existProxyAuth) {
136
+ throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor')
137
+ }
138
+ }
139
+
78
140
  module.exports = ProxyAgent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.3.0",
3
+ "version": "5.5.1",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -67,7 +67,7 @@
67
67
  "@sinonjs/fake-timers": "^9.1.2",
68
68
  "@types/node": "^17.0.29",
69
69
  "abort-controller": "^3.0.0",
70
- "busboy": "^0.3.1",
70
+ "busboy": "^1.6.0",
71
71
  "chai": "^4.3.4",
72
72
  "chai-as-promised": "^7.1.1",
73
73
  "chai-iterator": "^3.0.2",
@@ -77,8 +77,8 @@
77
77
  "delay": "^5.0.0",
78
78
  "docsify-cli": "^4.4.3",
79
79
  "formdata-node": "^4.3.1",
80
- "https-pem": "^2.0.0",
81
- "husky": "^7.0.2",
80
+ "https-pem": "^3.0.0",
81
+ "husky": "^8.0.1",
82
82
  "import-fresh": "^3.3.0",
83
83
  "jest": "^28.0.1",
84
84
  "jsfuzz": "^1.0.15",
@@ -1,16 +1,15 @@
1
- import { URL } from 'url'
2
- import { TLSSocket, TlsOptions } from 'tls'
3
- import { Socket } from 'net'
1
+ import {TLSSocket, ConnectionOptions} from 'tls'
2
+ import {IpcNetConnectOpts, Socket, TcpNetConnectOpts} from 'net'
4
3
 
5
4
  export = buildConnector
6
5
  declare function buildConnector (options?: buildConnector.BuildOptions): typeof buildConnector.connector
7
6
 
8
7
  declare namespace buildConnector {
9
- export interface BuildOptions extends TlsOptions {
8
+ export type BuildOptions = (ConnectionOptions | TcpNetConnectOpts | IpcNetConnectOpts) & {
10
9
  maxCachedSessions?: number | null;
11
10
  socketPath?: string | null;
12
11
  timeout?: number | null;
13
- servername?: string | null;
12
+ port?: number;
14
13
  }
15
14
 
16
15
  export interface Options {
@@ -3,7 +3,7 @@ import { Duplex, Readable, Writable } from 'stream'
3
3
  import { EventEmitter } from 'events'
4
4
  import { IncomingHttpHeaders } from 'http'
5
5
  import { Blob } from 'buffer'
6
- import BodyReadable from './readable'
6
+ import BodyReadable = require('./readable')
7
7
  import { FormData } from './formdata'
8
8
 
9
9
  type AbortSignal = unknown;
package/types/fetch.d.ts CHANGED
@@ -38,21 +38,21 @@ export interface BodyMixin {
38
38
  readonly text: () => Promise<string>
39
39
  }
40
40
 
41
- export interface HeadersIterator<T, TReturn = any, TNext = undefined> {
41
+ export interface SpecIterator<T, TReturn = any, TNext = undefined> {
42
42
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
43
43
  }
44
44
 
45
- export interface HeadersIterableIterator<T> extends HeadersIterator<T> {
46
- [Symbol.iterator](): HeadersIterableIterator<T>;
45
+ export interface SpecIterableIterator<T> extends SpecIterator<T> {
46
+ [Symbol.iterator](): SpecIterableIterator<T>;
47
47
  }
48
48
 
49
- export interface HeadersIterable<T> {
50
- [Symbol.iterator](): HeadersIterator<T>;
49
+ export interface SpecIterable<T> {
50
+ [Symbol.iterator](): SpecIterator<T>;
51
51
  }
52
52
 
53
53
  export type HeadersInit = string[][] | Record<string, string | ReadonlyArray<string>> | Headers
54
54
 
55
- export declare class Headers implements HeadersIterable<[string, string]> {
55
+ export declare class Headers implements SpecIterable<[string, string]> {
56
56
  constructor (init?: HeadersInit)
57
57
  readonly append: (name: string, value: string) => void
58
58
  readonly delete: (name: string) => void
@@ -64,10 +64,10 @@ export declare class Headers implements HeadersIterable<[string, string]> {
64
64
  thisArg?: unknown
65
65
  ) => void
66
66
 
67
- readonly keys: () => HeadersIterableIterator<string>
68
- readonly values: () => HeadersIterableIterator<string>
69
- readonly entries: () => HeadersIterableIterator<[string, string]>
70
- readonly [Symbol.iterator]: () => HeadersIterator<[string, string]>
67
+ readonly keys: () => SpecIterableIterator<string>
68
+ readonly values: () => SpecIterableIterator<string>
69
+ readonly entries: () => SpecIterableIterator<[string, string]>
70
+ readonly [Symbol.iterator]: () => SpecIterator<[string, string]>
71
71
  }
72
72
 
73
73
  export type RequestCache =
@@ -2,6 +2,7 @@
2
2
  /// <reference types="node" />
3
3
 
4
4
  import { File } from './file'
5
+ import { SpecIterator, SpecIterableIterator } from './fetch'
5
6
 
6
7
  /**
7
8
  * A `string` or `File` that represents a single value from a set of `FormData` key-value pairs.
@@ -73,32 +74,35 @@ export declare class FormData {
73
74
  delete(name: string): void
74
75
 
75
76
  /**
76
- * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object.
77
- * Each key is a `string`.
77
+ * Executes given callback function for each field of the FormData instance
78
78
  */
79
- keys(): Generator<string>
79
+ forEach: (
80
+ callbackfn: (value: FormDataEntryValue, key: string, iterable: FormData) => void,
81
+ thisArg?: unknown
82
+ ) => void
80
83
 
81
84
  /**
82
- * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs.
83
- * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
85
+ * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object.
86
+ * Each key is a `string`.
84
87
  */
85
- entries(): Generator<[string, FormDataEntryValue]>
88
+ keys: () => SpecIterableIterator<string>
86
89
 
87
90
  /**
88
91
  * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object.
89
92
  * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
90
93
  */
91
- values(): Generator<FormDataEntryValue>
94
+ values: () => SpecIterableIterator<FormDataEntryValue>
92
95
 
93
96
  /**
94
- * An alias for FormData#entries()
97
+ * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs.
98
+ * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
95
99
  */
96
- [Symbol.iterator](): Generator<[string, FormDataEntryValue], void>
100
+ entries: () => SpecIterableIterator<[string, FormDataEntryValue]>
97
101
 
98
102
  /**
99
- * Executes given callback function for each field of the FormData instance
103
+ * An alias for FormData#entries()
100
104
  */
101
- forEach(callback: (value: FormDataEntryValue, key: string, formData: FormData) => void, thisArg?: unknown): void
105
+ [Symbol.iterator]: () => SpecIterableIterator<[string, FormDataEntryValue]>
102
106
 
103
107
  readonly [Symbol.toStringTag]: string
104
108
  }
@@ -1,5 +1,5 @@
1
1
  import { IncomingHttpHeaders } from 'http'
2
- import Dispatcher from './dispatcher';
2
+ import Dispatcher = require('./dispatcher');
3
3
  import { BodyInit, Headers } from './fetch'
4
4
 
5
5
  export {
@@ -50,6 +50,8 @@ declare namespace MockInterceptor {
50
50
  body?: string | RegExp | ((body: string) => boolean);
51
51
  /** Headers to intercept on. */
52
52
  headers?: Record<string, string | RegExp | ((body: string) => boolean)> | ((headers: Record<string, string>) => boolean);
53
+ /** Query params to intercept on */
54
+ query?: Record<string, any>;
53
55
  }
54
56
  export interface MockDispatch<TData extends object = object, TError extends Error = Error> extends Options {
55
57
  times: number | null;
@@ -1,3 +1,4 @@
1
+ import { TlsOptions } from 'tls'
1
2
  import Agent = require('./agent')
2
3
  import Dispatcher = require('./dispatcher')
3
4
 
@@ -13,5 +14,8 @@ declare class ProxyAgent extends Dispatcher {
13
14
  declare namespace ProxyAgent {
14
15
  export interface Options extends Agent.Options {
15
16
  uri: string;
17
+ auth?: string;
18
+ requestTls?: TlsOptions & { servername?: string };
19
+ proxyTls?: TlsOptions & { servername?: string };
16
20
  }
17
21
  }