undici 5.3.0 → 5.4.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/README.md CHANGED
@@ -288,6 +288,15 @@ const headers = await fetch(url)
288
288
  .then(res => res.headers)
289
289
  ```
290
290
 
291
+ ##### Forbidden and Safelisted Header Names
292
+
293
+ * https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
294
+ * https://fetch.spec.whatwg.org/#forbidden-header-name
295
+ * https://fetch.spec.whatwg.org/#forbidden-response-header-name
296
+ * https://github.com/wintercg/fetch/issues/6
297
+
298
+ 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.
299
+
291
300
  ### `undici.upgrade([url, options]): Promise`
292
301
 
293
302
  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
 
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
  }
@@ -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,
@@ -243,8 +243,8 @@ function makeEntry (name, value, filename) {
243
243
  // object, representing the same bytes, whose name attribute value is "blob".
244
244
  if (isBlobLike(value) && !isFileLike(value)) {
245
245
  value = value instanceof Blob
246
- ? new File([value], 'blob')
247
- : new FileLike(value, 'blob')
246
+ ? new File([value], 'blob', value)
247
+ : new FileLike(value, 'blob', value)
248
248
  }
249
249
 
250
250
  // 4. If value is (now) a File object and filename is given, then set value to a
@@ -256,8 +256,8 @@ function makeEntry (name, value, filename) {
256
256
  // creating one more File instance doesn't make much sense....
257
257
  if (isFileLike(value) && filename != null) {
258
258
  value = value instanceof File
259
- ? new File([value], filename)
260
- : new FileLike(value, filename)
259
+ ? new File([value], filename, value)
260
+ : new FileLike(value, filename, value)
261
261
  }
262
262
 
263
263
  // 5. Set entry’s value to value.
@@ -6,10 +6,6 @@ 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')
13
9
 
14
10
  const kHeadersMap = Symbol('headers map')
15
11
  const kHeadersSortedMap = Symbol('headers map sorted')
@@ -115,6 +111,11 @@ class HeadersList {
115
111
  }
116
112
  }
117
113
 
114
+ clear () {
115
+ this[kHeadersMap].clear()
116
+ this[kHeadersSortedMap] = null
117
+ }
118
+
118
119
  append (name, value) {
119
120
  this[kHeadersSortedMap] = null
120
121
 
@@ -211,22 +212,11 @@ class Headers {
211
212
  )
212
213
  }
213
214
 
214
- const normalizedName = normalizeAndValidateHeaderName(String(name))
215
-
215
+ // Note: undici does not implement forbidden header names
216
216
  if (this[kGuard] === 'immutable') {
217
217
  throw new TypeError('immutable')
218
- } else if (
219
- this[kGuard] === 'request' &&
220
- forbiddenHeaderNames.includes(normalizedName)
221
- ) {
222
- return
223
218
  } else if (this[kGuard] === 'request-no-cors') {
224
219
  // TODO
225
- } else if (
226
- this[kGuard] === 'response' &&
227
- forbiddenResponseHeaderNames.includes(normalizedName)
228
- ) {
229
- return
230
220
  }
231
221
 
232
222
  return this[kHeadersList].append(String(name), String(value))
@@ -244,22 +234,11 @@ class Headers {
244
234
  )
245
235
  }
246
236
 
247
- const normalizedName = normalizeAndValidateHeaderName(String(name))
248
-
237
+ // Note: undici does not implement forbidden header names
249
238
  if (this[kGuard] === 'immutable') {
250
239
  throw new TypeError('immutable')
251
- } else if (
252
- this[kGuard] === 'request' &&
253
- forbiddenHeaderNames.includes(normalizedName)
254
- ) {
255
- return
256
240
  } else if (this[kGuard] === 'request-no-cors') {
257
241
  // TODO
258
- } else if (
259
- this[kGuard] === 'response' &&
260
- forbiddenResponseHeaderNames.includes(normalizedName)
261
- ) {
262
- return
263
242
  }
264
243
 
265
244
  return this[kHeadersList].delete(String(name))
@@ -307,20 +286,11 @@ class Headers {
307
286
  )
308
287
  }
309
288
 
289
+ // Note: undici does not implement forbidden header names
310
290
  if (this[kGuard] === 'immutable') {
311
291
  throw new TypeError('immutable')
312
- } else if (
313
- this[kGuard] === 'request' &&
314
- forbiddenHeaderNames.includes(String(name).toLocaleLowerCase())
315
- ) {
316
- return
317
292
  } else if (this[kGuard] === 'request-no-cors') {
318
293
  // TODO
319
- } else if (
320
- this[kGuard] === 'response' &&
321
- forbiddenResponseHeaderNames.includes(String(name).toLocaleLowerCase())
322
- ) {
323
- return
324
294
  }
325
295
 
326
296
  return this[kHeadersList].set(String(name), String(value))
@@ -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 {
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.3.0",
3
+ "version": "5.4.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": {
@@ -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",
@@ -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;
@@ -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;