undici 5.20.0 → 5.21.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
@@ -407,7 +407,7 @@ Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
407
407
 
408
408
  ## Workarounds
409
409
 
410
- ### Network address family autoselection.
410
+ ### Network address family autoselection.
411
411
 
412
412
  If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
413
413
  first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
@@ -19,6 +19,7 @@ Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
19
19
  * **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string.
20
20
  * **token** `string` (optional) - It can be passed by a string of token for authentication.
21
21
  * **auth** `string` (**deprecated**) - Use token.
22
+ * **clientFactory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
22
23
 
23
24
  Examples:
24
25
 
@@ -83,7 +84,8 @@ import { setGlobalDispatcher, request, ProxyAgent } from 'undici';
83
84
 
84
85
  const proxyAgent = new ProxyAgent({
85
86
  uri: 'my.proxy.server',
86
- token: 'Bearer xxxx'
87
+ // token: 'Bearer xxxx'
88
+ token: `Basic ${Buffer.from('username:password').toString('base64')}`
87
89
  });
88
90
  setGlobalDispatcher(proxyAgent);
89
91
 
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { finished } = require('stream')
3
+ const { finished, PassThrough } = require('stream')
4
4
  const {
5
5
  InvalidArgumentError,
6
6
  InvalidReturnValueError,
7
- RequestAbortedError
7
+ RequestAbortedError,
8
+ ResponseStatusCodeError
8
9
  } = require('../core/errors')
9
10
  const util = require('../core/util')
10
11
  const { AsyncResource } = require('async_hooks')
@@ -16,7 +17,7 @@ class StreamHandler extends AsyncResource {
16
17
  throw new InvalidArgumentError('invalid opts')
17
18
  }
18
19
 
19
- const { signal, method, opaque, body, onInfo, responseHeaders } = opts
20
+ const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
20
21
 
21
22
  try {
22
23
  if (typeof callback !== 'function') {
@@ -57,6 +58,7 @@ class StreamHandler extends AsyncResource {
57
58
  this.trailers = null
58
59
  this.body = body
59
60
  this.onInfo = onInfo || null
61
+ this.throwOnError = throwOnError || false
60
62
 
61
63
  if (util.isStream(body)) {
62
64
  body.on('error', (err) => {
@@ -76,8 +78,8 @@ class StreamHandler extends AsyncResource {
76
78
  this.context = context
77
79
  }
78
80
 
79
- onHeaders (statusCode, rawHeaders, resume) {
80
- const { factory, opaque, context } = this
81
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
82
+ const { factory, opaque, context, callback } = this
81
83
 
82
84
  if (statusCode < 200) {
83
85
  if (this.onInfo) {
@@ -96,6 +98,32 @@ class StreamHandler extends AsyncResource {
96
98
  context
97
99
  })
98
100
 
101
+ if (this.throwOnError && statusCode >= 400) {
102
+ const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
103
+ const chunks = []
104
+ const pt = new PassThrough()
105
+ pt
106
+ .on('data', (chunk) => chunks.push(chunk))
107
+ .on('end', () => {
108
+ const payload = Buffer.concat(chunks).toString('utf8')
109
+ this.runInAsyncScope(
110
+ callback,
111
+ null,
112
+ new ResponseStatusCodeError(
113
+ `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`,
114
+ statusCode,
115
+ headers,
116
+ payload
117
+ )
118
+ )
119
+ })
120
+ .on('error', (err) => {
121
+ this.onError(err)
122
+ })
123
+ this.res = pt
124
+ return
125
+ }
126
+
99
127
  if (
100
128
  !res ||
101
129
  typeof res.write !== 'function' ||
@@ -4,7 +4,7 @@
4
4
 
5
5
  const assert = require('assert')
6
6
  const { Readable } = require('stream')
7
- const { RequestAbortedError, NotSupportedError } = require('../core/errors')
7
+ const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors')
8
8
  const util = require('../core/util')
9
9
  const { ReadableStreamFrom, toUSVString } = require('../core/util')
10
10
 
@@ -146,15 +146,31 @@ module.exports = class BodyReadable extends Readable {
146
146
 
147
147
  async dump (opts) {
148
148
  let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
149
+ const signal = opts && opts.signal
150
+ const abortFn = () => {
151
+ this.destroy()
152
+ }
153
+ if (signal) {
154
+ if (typeof signal !== 'object' || !('aborted' in signal)) {
155
+ throw new InvalidArgumentError('signal must be an AbortSignal')
156
+ }
157
+ util.throwIfAborted(signal)
158
+ signal.addEventListener('abort', abortFn, { once: true })
159
+ }
149
160
  try {
150
161
  for await (const chunk of this) {
162
+ util.throwIfAborted(signal)
151
163
  limit -= Buffer.byteLength(chunk)
152
164
  if (limit < 0) {
153
165
  return
154
166
  }
155
167
  }
156
168
  } catch {
157
- // Do nothing...
169
+ util.throwIfAborted(signal)
170
+ } finally {
171
+ if (signal) {
172
+ signal.removeEventListener('abort', abortFn)
173
+ }
158
174
  }
159
175
  }
160
176
  }
package/lib/client.js CHANGED
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  'use strict'
2
4
 
3
5
  /* global WebAssembly */
@@ -85,7 +87,15 @@ try {
85
87
  channels.connected = { hasSubscribers: false }
86
88
  }
87
89
 
90
+ /**
91
+ * @type {import('../types/client').default}
92
+ */
88
93
  class Client extends DispatcherBase {
94
+ /**
95
+ *
96
+ * @param {string|URL} url
97
+ * @param {import('../types/client').Client.Options} options
98
+ */
89
99
  constructor (url, {
90
100
  interceptors,
91
101
  maxHeaderSize,
@@ -1658,6 +1668,8 @@ class AsyncWriter {
1658
1668
  process.emitWarning(new RequestContentLengthMismatchError())
1659
1669
  }
1660
1670
 
1671
+ socket.cork()
1672
+
1661
1673
  if (bytesWritten === 0) {
1662
1674
  if (!expectsPayload) {
1663
1675
  socket[kReset] = true
@@ -1678,6 +1690,8 @@ class AsyncWriter {
1678
1690
 
1679
1691
  const ret = socket.write(chunk)
1680
1692
 
1693
+ socket.uncork()
1694
+
1681
1695
  request.onBodySent(chunk)
1682
1696
 
1683
1697
  if (!ret) {
package/lib/core/util.js CHANGED
@@ -15,7 +15,7 @@ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(
15
15
  function nop () {}
16
16
 
17
17
  function isStream (obj) {
18
- return obj && typeof obj.pipe === 'function'
18
+ return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
19
19
  }
20
20
 
21
21
  // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
@@ -46,6 +46,12 @@ function buildURL (url, queryParams) {
46
46
  function parseURL (url) {
47
47
  if (typeof url === 'string') {
48
48
  url = new URL(url)
49
+
50
+ if (!/^https?:/.test(url.origin || url.protocol)) {
51
+ throw new InvalidArgumentError('invalid protocol')
52
+ }
53
+
54
+ return url
49
55
  }
50
56
 
51
57
  if (!url || typeof url !== 'object') {
@@ -375,23 +381,34 @@ function ReadableStreamFrom (iterable) {
375
381
 
376
382
  // The chunk should be a FormData instance and contains
377
383
  // all the required methods.
378
- function isFormDataLike (chunk) {
379
- return (chunk &&
380
- chunk.constructor && chunk.constructor.name === 'FormData' &&
381
- typeof chunk === 'object' &&
382
- (typeof chunk.append === 'function' &&
383
- typeof chunk.delete === 'function' &&
384
- typeof chunk.get === 'function' &&
385
- typeof chunk.getAll === 'function' &&
386
- typeof chunk.has === 'function' &&
387
- typeof chunk.set === 'function' &&
388
- typeof chunk.entries === 'function' &&
389
- typeof chunk.keys === 'function' &&
390
- typeof chunk.values === 'function' &&
391
- typeof chunk.forEach === 'function')
384
+ function isFormDataLike (object) {
385
+ return (
386
+ object &&
387
+ typeof object === 'object' &&
388
+ typeof object.append === 'function' &&
389
+ typeof object.delete === 'function' &&
390
+ typeof object.get === 'function' &&
391
+ typeof object.getAll === 'function' &&
392
+ typeof object.has === 'function' &&
393
+ typeof object.set === 'function' &&
394
+ object[Symbol.toStringTag] === 'FormData'
392
395
  )
393
396
  }
394
397
 
398
+ function throwIfAborted (signal) {
399
+ if (!signal) { return }
400
+ if (typeof signal.throwIfAborted === 'function') {
401
+ signal.throwIfAborted()
402
+ } else {
403
+ if (signal.aborted) {
404
+ // DOMException not available < v17.0.0
405
+ const err = new Error('The operation was aborted')
406
+ err.name = 'AbortError'
407
+ throw err
408
+ }
409
+ }
410
+ }
411
+
395
412
  const kEnumerableProperty = Object.create(null)
396
413
  kEnumerableProperty.enumerable = true
397
414
 
@@ -423,6 +440,7 @@ module.exports = {
423
440
  getSocketInfo,
424
441
  isFormDataLike,
425
442
  buildURL,
443
+ throwIfAborted,
426
444
  nodeMajor,
427
445
  nodeMinor,
428
446
  nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
@@ -1,6 +1,5 @@
1
1
  const assert = require('assert')
2
2
  const { atob } = require('buffer')
3
- const { format } = require('url')
4
3
  const { isValidHTTPToken, isomorphicDecode } = require('./util')
5
4
 
6
5
  const encoder = new TextEncoder()
@@ -118,7 +117,17 @@ function dataURLProcessor (dataURL) {
118
117
  * @param {boolean} excludeFragment
119
118
  */
120
119
  function URLSerializer (url, excludeFragment = false) {
121
- return format(url, { fragment: !excludeFragment })
120
+ const href = url.href
121
+
122
+ if (!excludeFragment) {
123
+ return href
124
+ }
125
+
126
+ const hash = href.lastIndexOf('#')
127
+ if (hash === -1) {
128
+ return href
129
+ }
130
+ return href.slice(0, hash)
122
131
  }
123
132
 
124
133
  // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
@@ -297,7 +297,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
297
297
  // capability.
298
298
  // TODO: given global’s relevant settings object’s cross-origin isolated
299
299
  // capability?
300
- response.timingInfo.endTime = coarsenedSharedCurrentTime()
300
+ timingInfo.endTime = coarsenedSharedCurrentTime()
301
301
 
302
302
  // 10. Set response’s timing info to timingInfo.
303
303
  response.timingInfo = timingInfo
@@ -9,7 +9,8 @@ const util = require('../core/util')
9
9
  const {
10
10
  isValidHTTPToken,
11
11
  sameOrigin,
12
- normalizeMethod
12
+ normalizeMethod,
13
+ makePolicyContainer
13
14
  } = require('./util')
14
15
  const {
15
16
  forbiddenMethods,
@@ -51,10 +52,14 @@ class Request {
51
52
  input = webidl.converters.RequestInfo(input)
52
53
  init = webidl.converters.RequestInit(init)
53
54
 
54
- // TODO
55
+ // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
55
56
  this[kRealm] = {
56
57
  settingsObject: {
57
- baseUrl: getGlobalOrigin()
58
+ baseUrl: getGlobalOrigin(),
59
+ get origin () {
60
+ return this.baseUrl?.origin
61
+ },
62
+ policyContainer: makePolicyContainer()
58
63
  }
59
64
  }
60
65
 
@@ -349,14 +354,17 @@ class Request {
349
354
  if (signal.aborted) {
350
355
  ac.abort(signal.reason)
351
356
  } else {
352
- const acRef = new WeakRef(ac)
353
357
  const abort = function () {
354
- acRef.deref()?.abort(this.reason)
358
+ ac.abort(this.reason)
355
359
  }
356
360
 
357
- if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
358
- setMaxListeners(100, signal)
359
- }
361
+ // Third-party AbortControllers may not work with these.
362
+ // See https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619
363
+ try {
364
+ if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
365
+ setMaxListeners(100, signal)
366
+ }
367
+ } catch {}
360
368
 
361
369
  signal.addEventListener('abort', abort, { once: true })
362
370
  requestFinalizer.register(this, { signal, abort })
package/lib/fetch/util.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants')
4
+ const { getGlobalOrigin } = require('./global')
4
5
  const { performance } = require('perf_hooks')
5
6
  const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
6
7
  const assert = require('assert')
@@ -36,9 +37,11 @@ function responseLocationURL (response, requestFragment) {
36
37
  // `Location` and response’s header list.
37
38
  let location = response.headersList.get('location')
38
39
 
39
- // 3. If location is a value, then set location to the result of parsing
40
- // location with response’s URL.
41
- location = location ? new URL(location, responseURL(response)) : null
40
+ // 3. If location is a header value, then set location to the result of
41
+ // parsing location with response’s URL.
42
+ if (location !== null && isValidHeaderValue(location)) {
43
+ location = new URL(location, responseURL(response))
44
+ }
42
45
 
43
46
  // 4. If location is a URL whose fragment is null, then set location’s
44
47
  // fragment to requestFragment.
@@ -267,7 +270,7 @@ function appendRequestOriginHeader (request) {
267
270
  // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
268
271
  if (request.responseTainting === 'cors' || request.mode === 'websocket') {
269
272
  if (serializedOrigin) {
270
- request.headersList.append('Origin', serializedOrigin)
273
+ request.headersList.append('origin', serializedOrigin)
271
274
  }
272
275
 
273
276
  // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
@@ -298,7 +301,7 @@ function appendRequestOriginHeader (request) {
298
301
 
299
302
  if (serializedOrigin) {
300
303
  // 2. Append (`Origin`, serializedOrigin) to request’s header list.
301
- request.headersList.append('Origin', serializedOrigin)
304
+ request.headersList.append('origin', serializedOrigin)
302
305
  }
303
306
  }
304
307
  }
@@ -327,14 +330,17 @@ function createOpaqueTimingInfo (timingInfo) {
327
330
 
328
331
  // https://html.spec.whatwg.org/multipage/origin.html#policy-container
329
332
  function makePolicyContainer () {
330
- // TODO
331
- return {}
333
+ // Note: the fetch spec doesn't make use of embedder policy or CSP list
334
+ return {
335
+ referrerPolicy: 'strict-origin-when-cross-origin'
336
+ }
332
337
  }
333
338
 
334
339
  // https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
335
- function clonePolicyContainer () {
336
- // TODO
337
- return {}
340
+ function clonePolicyContainer (policyContainer) {
341
+ return {
342
+ referrerPolicy: policyContainer.referrerPolicy
343
+ }
338
344
  }
339
345
 
340
346
  // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
@@ -342,104 +348,76 @@ function determineRequestsReferrer (request) {
342
348
  // 1. Let policy be request's referrer policy.
343
349
  const policy = request.referrerPolicy
344
350
 
345
- // Return no-referrer when empty or policy says so
346
- if (policy == null || policy === '' || policy === 'no-referrer') {
347
- return 'no-referrer'
348
- }
351
+ // Note: policy cannot (shouldn't) be null or an empty string.
352
+ assert(policy)
353
+
354
+ // 2. Let environment be request’s client.
349
355
 
350
- // 2. Let environment be the request client
351
- const environment = request.client
352
356
  let referrerSource = null
353
357
 
354
- /**
355
- * 3, Switch on request’s referrer:
356
- "client"
357
- If environment’s global object is a Window object, then
358
- Let document be the associated Document of environment’s global object.
359
- If document’s origin is an opaque origin, return no referrer.
360
- While document is an iframe srcdoc document,
361
- let document be document’s browsing context’s browsing context container’s node document.
362
- Let referrerSource be document’s URL.
363
-
364
- Otherwise, let referrerSource be environment’s creation URL.
365
-
366
- a URL
367
- Let referrerSource be request’s referrer.
368
- */
358
+ // 3. Switch on request’s referrer:
369
359
  if (request.referrer === 'client') {
370
- // Not defined in Node but part of the spec
371
- if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line
372
- const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin
373
-
374
- // If document’s origin is an opaque origin, return no referrer.
375
- if (origin == null || origin === 'null') return 'no-referrer'
376
-
377
- // Let referrerSource be document’s URL.
378
- referrerSource = new URL(environment.globalObject.location.href)
379
- } else {
380
- // 3(a)(II) If environment's global object is not Window,
381
- // Let referrerSource be environments creationURL
382
- if (environment?.globalObject?.location == null) {
383
- return 'no-referrer'
384
- }
360
+ // Note: node isn't a browser and doesn't implement document/iframes,
361
+ // so we bypass this step and replace it with our own.
362
+
363
+ const globalOrigin = getGlobalOrigin()
385
364
 
386
- referrerSource = new URL(environment.globalObject.location.href)
365
+ if (!globalOrigin || globalOrigin.origin === 'null') {
366
+ return 'no-referrer'
387
367
  }
368
+
369
+ // note: we need to clone it as it's mutated
370
+ referrerSource = new URL(globalOrigin)
388
371
  } else if (request.referrer instanceof URL) {
389
- // 3(b) If requests's referrer is a URL instance, then make
390
- // referrerSource be requests's referrer.
372
+ // Let referrerSource be request’s referrer.
391
373
  referrerSource = request.referrer
392
- } else {
393
- // If referrerSource neither client nor instance of URL
394
- // then return "no-referrer".
395
- return 'no-referrer'
396
374
  }
397
375
 
398
- const urlProtocol = referrerSource.protocol
376
+ // 4. Let request’s referrerURL be the result of stripping referrerSource for
377
+ // use as a referrer.
378
+ let referrerURL = stripURLForReferrer(referrerSource)
399
379
 
400
- // If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file")
401
- // then return "no-referrer".
402
- if (
403
- urlProtocol === 'about:' || urlProtocol === 'data:' ||
404
- urlProtocol === 'blob:'
405
- ) {
406
- return 'no-referrer'
380
+ // 5. Let referrerOrigin be the result of stripping referrerSource for use as
381
+ // a referrer, with the origin-only flag set to true.
382
+ const referrerOrigin = stripURLForReferrer(referrerSource, true)
383
+
384
+ // 6. If the result of serializing referrerURL is a string whose length is
385
+ // greater than 4096, set referrerURL to referrerOrigin.
386
+ if (referrerURL.toString().length > 4096) {
387
+ referrerURL = referrerOrigin
407
388
  }
408
389
 
409
- let temp
410
- let referrerOrigin
411
- // 4. Let requests's referrerURL be the result of stripping referrer
412
- // source for use as referrer (using util function, without origin only)
413
- const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096
414
- // 5. Let referrerOrigin be the result of stripping referrer
415
- // source for use as referrer (using util function, with originOnly true)
416
- ? (referrerOrigin = stripURLForReferrer(referrerSource, true))
417
- // 6. If result of seralizing referrerUrl is a string whose length is greater than
418
- // 4096, then set referrerURL to referrerOrigin
419
- : temp
420
- const areSameOrigin = sameOrigin(request, referrerUrl)
421
- const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) &&
390
+ const areSameOrigin = sameOrigin(request, referrerURL)
391
+ const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
422
392
  !isURLPotentiallyTrustworthy(request.url)
423
393
 
424
- // NOTE: How to treat step 7?
425
394
  // 8. Execute the switch statements corresponding to the value of policy:
426
395
  switch (policy) {
427
396
  case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
428
- case 'unsafe-url': return referrerUrl
397
+ case 'unsafe-url': return referrerURL
429
398
  case 'same-origin':
430
399
  return areSameOrigin ? referrerOrigin : 'no-referrer'
431
400
  case 'origin-when-cross-origin':
432
- return areSameOrigin ? referrerUrl : referrerOrigin
433
- case 'strict-origin-when-cross-origin':
434
- /**
435
- * 1. If the origin of referrerURL and the origin of request’s current URL are the same,
436
- * then return referrerURL.
437
- * 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a
438
- * potentially trustworthy URL, then return no referrer.
439
- * 3. Return referrerOrigin
440
- */
441
- if (areSameOrigin) return referrerOrigin
442
- // else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
401
+ return areSameOrigin ? referrerURL : referrerOrigin
402
+ case 'strict-origin-when-cross-origin': {
403
+ const currentURL = requestCurrentURL(request)
404
+
405
+ // 1. If the origin of referrerURL and the origin of request’s current
406
+ // URL are the same, then return referrerURL.
407
+ if (sameOrigin(referrerURL, currentURL)) {
408
+ return referrerURL
409
+ }
410
+
411
+ // 2. If referrerURL is a potentially trustworthy URL and request’s
412
+ // current URL is not a potentially trustworthy URL, then return no
413
+ // referrer.
414
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
415
+ return 'no-referrer'
416
+ }
417
+
418
+ // 3. Return referrerOrigin.
419
+ return referrerOrigin
420
+ }
443
421
  case 'strict-origin': // eslint-disable-line
444
422
  /**
445
423
  * 1. If referrerURL is a potentially trustworthy URL and
@@ -458,15 +436,42 @@ function determineRequestsReferrer (request) {
458
436
  default: // eslint-disable-line
459
437
  return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
460
438
  }
439
+ }
461
440
 
462
- function stripURLForReferrer (url, originOnly = false) {
463
- const urlObject = new URL(url.href)
464
- urlObject.username = ''
465
- urlObject.password = ''
466
- urlObject.hash = ''
441
+ /**
442
+ * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
443
+ * @param {URL} url
444
+ * @param {boolean|undefined} originOnly
445
+ */
446
+ function stripURLForReferrer (url, originOnly) {
447
+ // 1. Assert: url is a URL.
448
+ assert(url instanceof URL)
467
449
 
468
- return originOnly ? urlObject.origin : urlObject.href
450
+ // 2. If url’s scheme is a local scheme, then return no referrer.
451
+ if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
452
+ return 'no-referrer'
469
453
  }
454
+
455
+ // 3. Set url’s username to the empty string.
456
+ url.username = ''
457
+
458
+ // 4. Set url’s password to the empty string.
459
+ url.password = ''
460
+
461
+ // 5. Set url’s fragment to null.
462
+ url.hash = ''
463
+
464
+ // 6. If the origin-only flag is true, then:
465
+ if (originOnly) {
466
+ // 1. Set url’s path to « the empty string ».
467
+ url.pathname = ''
468
+
469
+ // 2. Set url’s query to null.
470
+ url.search = ''
471
+ }
472
+
473
+ // 7. Return url.
474
+ return url
470
475
  }
471
476
 
472
477
  function isURLPotentiallyTrustworthy (url) {
@@ -2,9 +2,13 @@
2
2
 
3
3
  /**
4
4
  * @see https://encoding.spec.whatwg.org/#concept-encoding-get
5
- * @param {string} label
5
+ * @param {string|undefined} label
6
6
  */
7
7
  function getEncoding (label) {
8
+ if (!label) {
9
+ return 'failure'
10
+ }
11
+
8
12
  // 1. Remove any leading and trailing ASCII whitespace from label.
9
13
  // 2. If label is an ASCII case-insensitive match for any of the
10
14
  // labels listed in the table below, then return the
@@ -3,7 +3,7 @@
3
3
  const { kProxy, kClose, kDestroy, kInterceptors } = require('./core/symbols')
4
4
  const { URL } = require('url')
5
5
  const Agent = require('./agent')
6
- const Client = require('./client')
6
+ const Pool = require('./pool')
7
7
  const DispatcherBase = require('./dispatcher-base')
8
8
  const { InvalidArgumentError, RequestAbortedError } = require('./core/errors')
9
9
  const buildConnector = require('./core/connect')
@@ -34,6 +34,10 @@ function buildProxyOptions (opts) {
34
34
  }
35
35
  }
36
36
 
37
+ function defaultFactory (origin, opts) {
38
+ return new Pool(origin, opts)
39
+ }
40
+
37
41
  class ProxyAgent extends DispatcherBase {
38
42
  constructor (opts) {
39
43
  super(opts)
@@ -51,6 +55,12 @@ class ProxyAgent extends DispatcherBase {
51
55
  throw new InvalidArgumentError('Proxy opts.uri is mandatory')
52
56
  }
53
57
 
58
+ const { clientFactory = defaultFactory } = opts
59
+
60
+ if (typeof clientFactory !== 'function') {
61
+ throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
62
+ }
63
+
54
64
  this[kRequestTls] = opts.requestTls
55
65
  this[kProxyTls] = opts.proxyTls
56
66
  this[kProxyHeaders] = opts.headers || {}
@@ -69,7 +79,7 @@ class ProxyAgent extends DispatcherBase {
69
79
 
70
80
  const connect = buildConnector({ ...opts.proxyTls })
71
81
  this[kConnectEndpoint] = buildConnector({ ...opts.requestTls })
72
- this[kClient] = new Client(resolvedUrl, { connect })
82
+ this[kClient] = clientFactory(resolvedUrl, { connect })
73
83
  this[kAgent] = new Agent({
74
84
  ...opts,
75
85
  connect: async (opts, callback) => {
package/lib/timers.js CHANGED
@@ -13,13 +13,15 @@ function onTimeout () {
13
13
  while (idx < len) {
14
14
  const timer = fastTimers[idx]
15
15
 
16
- if (timer.expires && fastNow >= timer.expires) {
17
- timer.expires = 0
16
+ if (timer.state === 0) {
17
+ timer.state = fastNow + timer.delay
18
+ } else if (timer.state > 0 && fastNow >= timer.state) {
19
+ timer.state = -1
18
20
  timer.callback(timer.opaque)
19
21
  }
20
22
 
21
- if (timer.expires === 0) {
22
- timer.active = false
23
+ if (timer.state === -1) {
24
+ timer.state = -2
23
25
  if (idx !== len - 1) {
24
26
  fastTimers[idx] = fastTimers.pop()
25
27
  } else {
@@ -53,37 +55,43 @@ class Timeout {
53
55
  this.callback = callback
54
56
  this.delay = delay
55
57
  this.opaque = opaque
56
- this.expires = 0
57
- this.active = false
58
+
59
+ // -2 not in timer list
60
+ // -1 in timer list but inactive
61
+ // 0 in timer list waiting for time
62
+ // > 0 in timer list waiting for time to expire
63
+ this.state = -2
58
64
 
59
65
  this.refresh()
60
66
  }
61
67
 
62
68
  refresh () {
63
- if (!this.active) {
64
- this.active = true
69
+ if (this.state === -2) {
65
70
  fastTimers.push(this)
66
71
  if (!fastNowTimeout || fastTimers.length === 1) {
67
72
  refreshTimeout()
68
- fastNow = Date.now()
69
73
  }
70
74
  }
71
75
 
72
- this.expires = fastNow + this.delay
76
+ this.state = 0
73
77
  }
74
78
 
75
79
  clear () {
76
- this.expires = 0
80
+ this.state = -1
77
81
  }
78
82
  }
79
83
 
80
84
  module.exports = {
81
85
  setTimeout (callback, delay, opaque) {
82
- return new Timeout(callback, delay, opaque)
86
+ return delay < 1e3
87
+ ? setTimeout(callback, delay, opaque)
88
+ : new Timeout(callback, delay, opaque)
83
89
  },
84
90
  clearTimeout (timeout) {
85
- if (timeout && timeout.clear) {
91
+ if (timeout instanceof Timeout) {
86
92
  timeout.clear()
93
+ } else {
94
+ clearTimeout(timeout)
87
95
  }
88
96
  }
89
97
  }
@@ -5,19 +5,15 @@ const diagnosticsChannel = require('diagnostics_channel')
5
5
  const { uid, states } = require('./constants')
6
6
  const {
7
7
  kReadyState,
8
- kResponse,
9
- kExtensions,
10
- kProtocol,
11
8
  kSentClose,
12
9
  kByteParser,
13
10
  kReceivedClose
14
11
  } = require('./symbols')
15
12
  const { fireEvent, failWebsocketConnection } = require('./util')
16
13
  const { CloseEvent } = require('./events')
17
- const { ByteParser } = require('./receiver')
18
14
  const { makeRequest } = require('../fetch/request')
19
15
  const { fetching } = require('../fetch/index')
20
- const { getGlobalDispatcher } = require('../..')
16
+ const { getGlobalDispatcher } = require('../global')
21
17
 
22
18
  const channels = {}
23
19
  channels.open = diagnosticsChannel.channel('undici:websocket:open')
@@ -29,8 +25,9 @@ channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error
29
25
  * @param {URL} url
30
26
  * @param {string|string[]} protocols
31
27
  * @param {import('./websocket').WebSocket} ws
28
+ * @param {(response: any) => void} onEstablish
32
29
  */
33
- function establishWebSocketConnection (url, protocols, ws) {
30
+ function establishWebSocketConnection (url, protocols, ws, onEstablish) {
34
31
  // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s
35
32
  // scheme is "ws", and to "https" otherwise.
36
33
  const requestURL = url
@@ -173,67 +170,25 @@ function establishWebSocketConnection (url, protocols, ws) {
173
170
  return
174
171
  }
175
172
 
176
- // processResponse is called when the "response’s header list has been received and initialized."
177
- // once this happens, the connection is open
178
- ws[kResponse] = response
179
-
180
- const parser = new ByteParser(ws)
181
- response.socket.ws = ws // TODO: use symbol
182
- ws[kByteParser] = parser
183
-
184
- whenConnectionEstablished(ws)
185
-
186
173
  response.socket.on('data', onSocketData)
187
174
  response.socket.on('close', onSocketClose)
188
175
  response.socket.on('error', onSocketError)
189
176
 
190
- parser.on('drain', onParserDrain)
177
+ if (channels.open.hasSubscribers) {
178
+ channels.open.publish({
179
+ address: response.socket.address(),
180
+ protocol: secProtocol,
181
+ extensions: secExtension
182
+ })
183
+ }
184
+
185
+ onEstablish(response)
191
186
  }
192
187
  })
193
188
 
194
189
  return controller
195
190
  }
196
191
 
197
- /**
198
- * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
199
- * @param {import('./websocket').WebSocket} ws
200
- */
201
- function whenConnectionEstablished (ws) {
202
- const { [kResponse]: response } = ws
203
-
204
- // 1. Change the ready state to OPEN (1).
205
- ws[kReadyState] = states.OPEN
206
-
207
- // 2. Change the extensions attribute’s value to the extensions in use, if
208
- // it is not the null value.
209
- // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
210
- const extensions = response.headersList.get('sec-websocket-extensions')
211
-
212
- if (extensions !== null) {
213
- ws[kExtensions] = extensions
214
- }
215
-
216
- // 3. Change the protocol attribute’s value to the subprotocol in use, if
217
- // it is not the null value.
218
- // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
219
- const protocol = response.headersList.get('sec-websocket-protocol')
220
-
221
- if (protocol !== null) {
222
- ws[kProtocol] = protocol
223
- }
224
-
225
- // 4. Fire an event named open at the WebSocket object.
226
- fireEvent('open', ws)
227
-
228
- if (channels.open.hasSubscribers) {
229
- channels.open.publish({
230
- address: response.socket.address(),
231
- protocol,
232
- extensions
233
- })
234
- }
235
- }
236
-
237
192
  /**
238
193
  * @param {Buffer} chunk
239
194
  */
@@ -243,10 +198,6 @@ function onSocketData (chunk) {
243
198
  }
244
199
  }
245
200
 
246
- function onParserDrain () {
247
- this.ws[kResponse].socket.resume()
248
- }
249
-
250
201
  /**
251
202
  * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
252
203
  * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
@@ -5,10 +5,7 @@ module.exports = {
5
5
  kReadyState: Symbol('ready state'),
6
6
  kController: Symbol('controller'),
7
7
  kResponse: Symbol('response'),
8
- kExtensions: Symbol('extensions'),
9
- kProtocol: Symbol('protocol'),
10
8
  kBinaryType: Symbol('binary type'),
11
- kClosingFrame: Symbol('closing frame'),
12
9
  kSentClose: Symbol('sent close'),
13
10
  kReceivedClose: Symbol('received close'),
14
11
  kByteParser: Symbol('byte parser')
@@ -8,15 +8,15 @@ const {
8
8
  kWebSocketURL,
9
9
  kReadyState,
10
10
  kController,
11
- kExtensions,
12
- kProtocol,
13
11
  kBinaryType,
14
12
  kResponse,
15
- kSentClose
13
+ kSentClose,
14
+ kByteParser
16
15
  } = require('./symbols')
17
- const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection } = require('./util')
16
+ const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util')
18
17
  const { establishWebSocketConnection } = require('./connection')
19
18
  const { WebsocketFrameSend } = require('./frame')
19
+ const { ByteParser } = require('./receiver')
20
20
  const { kEnumerableProperty, isBlobLike } = require('../core/util')
21
21
  const { types } = require('util')
22
22
 
@@ -32,6 +32,8 @@ class WebSocket extends EventTarget {
32
32
  }
33
33
 
34
34
  #bufferedAmount = 0
35
+ #protocol = ''
36
+ #extensions = ''
35
37
 
36
38
  /**
37
39
  * @param {string} url
@@ -104,7 +106,12 @@ class WebSocket extends EventTarget {
104
106
 
105
107
  // 1. Establish a WebSocket connection given urlRecord, protocols,
106
108
  // and client.
107
- this[kController] = establishWebSocketConnection(urlRecord, protocols, this)
109
+ this[kController] = establishWebSocketConnection(
110
+ urlRecord,
111
+ protocols,
112
+ this,
113
+ (response) => this.#onConnectionEstablished(response)
114
+ )
108
115
 
109
116
  // Each WebSocket object has an associated ready state, which is a
110
117
  // number representing the state of the connection. Initially it must
@@ -112,10 +119,8 @@ class WebSocket extends EventTarget {
112
119
  this[kReadyState] = WebSocket.CONNECTING
113
120
 
114
121
  // The extensions attribute must initially return the empty string.
115
- this[kExtensions] = ''
116
122
 
117
123
  // The protocol attribute must initially return the empty string.
118
- this[kProtocol] = ''
119
124
 
120
125
  // Each WebSocket object has an associated binary type, which is a
121
126
  // BinaryType. Initially it must be "blob".
@@ -368,13 +373,13 @@ class WebSocket extends EventTarget {
368
373
  get extensions () {
369
374
  webidl.brandCheck(this, WebSocket)
370
375
 
371
- return this[kExtensions]
376
+ return this.#extensions
372
377
  }
373
378
 
374
379
  get protocol () {
375
380
  webidl.brandCheck(this, WebSocket)
376
381
 
377
- return this[kProtocol]
382
+ return this.#protocol
378
383
  }
379
384
 
380
385
  get onopen () {
@@ -476,6 +481,47 @@ class WebSocket extends EventTarget {
476
481
  this[kBinaryType] = type
477
482
  }
478
483
  }
484
+
485
+ /**
486
+ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
487
+ */
488
+ #onConnectionEstablished (response) {
489
+ // processResponse is called when the "response’s header list has been received and initialized."
490
+ // once this happens, the connection is open
491
+ this[kResponse] = response
492
+
493
+ const parser = new ByteParser(this)
494
+ parser.on('drain', function onParserDrain () {
495
+ this.ws[kResponse].socket.resume()
496
+ })
497
+
498
+ response.socket.ws = this
499
+ this[kByteParser] = parser
500
+
501
+ // 1. Change the ready state to OPEN (1).
502
+ this[kReadyState] = states.OPEN
503
+
504
+ // 2. Change the extensions attribute’s value to the extensions in use, if
505
+ // it is not the null value.
506
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1
507
+ const extensions = response.headersList.get('sec-websocket-extensions')
508
+
509
+ if (extensions !== null) {
510
+ this.#extensions = extensions
511
+ }
512
+
513
+ // 3. Change the protocol attribute’s value to the subprotocol in use, if
514
+ // it is not the null value.
515
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9
516
+ const protocol = response.headersList.get('sec-websocket-protocol')
517
+
518
+ if (protocol !== null) {
519
+ this.#protocol = protocol
520
+ }
521
+
522
+ // 4. Fire an event named open at the WebSocket object.
523
+ fireEvent('open', this)
524
+ }
479
525
  }
480
526
 
481
527
  // https://websockets.spec.whatwg.org/#dom-websocket-connecting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "5.20.0",
3
+ "version": "5.21.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": {
@@ -86,6 +86,7 @@
86
86
  "husky": "^8.0.1",
87
87
  "import-fresh": "^3.3.0",
88
88
  "jest": "^29.0.2",
89
+ "jsdom": "^21.1.0",
89
90
  "jsfuzz": "^1.0.15",
90
91
  "mocha": "^10.0.0",
91
92
  "p-timeout": "^3.2.0",
@@ -112,8 +113,7 @@
112
113
  "ignore": [
113
114
  "lib/llhttp/constants.js",
114
115
  "lib/llhttp/utils.js",
115
- "test/wpt/tests",
116
- "test/wpt/runner/resources"
116
+ "test/wpt/tests"
117
117
  ]
118
118
  },
119
119
  "tsd": {
@@ -5,10 +5,10 @@ import { URL } from 'url'
5
5
  export default BalancedPool
6
6
 
7
7
  declare class BalancedPool extends Dispatcher {
8
- constructor(url: string | URL | string[], options?: Pool.Options);
8
+ constructor(url: string | string[] | URL | URL[], options?: Pool.Options);
9
9
 
10
- addUpstream(upstream: string): BalancedPool;
11
- removeUpstream(upstream: string): BalancedPool;
10
+ addUpstream(upstream: string | URL): BalancedPool;
11
+ removeUpstream(upstream: string | URL): BalancedPool;
12
12
  upstreams: Array<string>;
13
13
 
14
14
  /** `true` after `pool.close()` has been called. */
package/types/client.d.ts CHANGED
@@ -4,10 +4,10 @@ import Dispatcher from './dispatcher'
4
4
  import DispatchInterceptor from './dispatcher'
5
5
  import buildConnector from "./connector";
6
6
 
7
- export default Client
8
-
9
- /** A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */
10
- declare class Client extends Dispatcher {
7
+ /**
8
+ * A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
9
+ */
10
+ export class Client extends Dispatcher {
11
11
  constructor(url: string | URL, options?: Client.Options);
12
12
  /** Property to get and set the pipelining factor. */
13
13
  pipelining: number;
@@ -17,40 +17,62 @@ declare class Client extends Dispatcher {
17
17
  destroyed: boolean;
18
18
  }
19
19
 
20
- declare namespace Client {
20
+ export declare namespace Client {
21
+ export interface OptionsInterceptors {
22
+ Client: readonly DispatchInterceptor[];
23
+ }
21
24
  export interface Options {
25
+ /** TODO */
26
+ interceptors?: OptionsInterceptors;
27
+ /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */
28
+ maxHeaderSize?: number;
29
+ /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */
30
+ headersTimeout?: number;
31
+ /** @deprecated unsupported socketTimeout, use headersTimeout & bodyTimeout instead */
32
+ socketTimeout?: never;
33
+ /** @deprecated unsupported requestTimeout, use headersTimeout & bodyTimeout instead */
34
+ requestTimeout?: never;
35
+ /** TODO */
36
+ connectTimeout?: number;
37
+ /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */
38
+ bodyTimeout?: number;
39
+ /** @deprecated unsupported idleTimeout, use keepAliveTimeout instead */
40
+ idleTimeout?: never;
41
+ /** @deprecated unsupported keepAlive, use pipelining=0 instead */
42
+ keepAlive?: never;
22
43
  /** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */
23
- keepAliveTimeout?: number | null;
44
+ keepAliveTimeout?: number;
45
+ /** @deprecated unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead */
46
+ maxKeepAliveTimeout?: never;
24
47
  /** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */
25
- keepAliveMaxTimeout?: number | null;
48
+ keepAliveMaxTimeout?: number;
26
49
  /** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */
27
- keepAliveTimeoutThreshold?: number | null;
50
+ keepAliveTimeoutThreshold?: number;
51
+ /** TODO */
52
+ socketPath?: string;
28
53
  /** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */
29
- pipelining?: number | null;
30
- /** **/
31
- connect?: buildConnector.BuildOptions | buildConnector.connector | null;
32
- /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */
33
- maxHeaderSize?: number | null;
34
- /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */
35
- bodyTimeout?: number | null;
36
- /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */
37
- headersTimeout?: number | null;
54
+ pipelining?: number;
55
+ /** @deprecated use the connect option instead */
56
+ tls?: never;
38
57
  /** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */
39
58
  strictContentLength?: boolean;
40
- /** @deprecated use the connect option instead */
41
- tls?: TlsOptions | null;
42
- /** */
59
+ /** TODO */
60
+ maxCachedSessions?: number;
61
+ /** TODO */
62
+ maxRedirections?: number;
63
+ /** TODO */
64
+ connect?: buildConnector.BuildOptions | buildConnector.connector;
65
+ /** TODO */
43
66
  maxRequestsPerClient?: number;
67
+ /** TODO */
68
+ localAddress?: string;
44
69
  /** Max response body size in bytes, -1 is disabled */
45
- maxResponseSize?: number | null;
70
+ maxResponseSize?: number;
46
71
  /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */
47
72
  autoSelectFamily?: boolean;
48
73
  /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */
49
- autoSelectFamilyAttemptTimeout?: number;
50
-
51
- interceptors?: {Client: readonly DispatchInterceptor[] | undefined}
74
+ autoSelectFamilyAttemptTimeout?: number;
52
75
  }
53
-
54
76
  export interface SocketInfo {
55
77
  localAddress?: string
56
78
  localPort?: number
@@ -61,6 +83,6 @@ declare namespace Client {
61
83
  bytesWritten?: number
62
84
  bytesRead?: number
63
85
  }
64
-
65
-
66
86
  }
87
+
88
+ export default Client;
@@ -1,7 +1,9 @@
1
1
  import Agent from './agent'
2
2
  import buildConnector from './connector';
3
+ import Client from './client'
3
4
  import Dispatcher from './dispatcher'
4
5
  import { IncomingHttpHeaders } from './header'
6
+ import Pool from './pool'
5
7
 
6
8
  export default ProxyAgent
7
9
 
@@ -23,5 +25,6 @@ declare namespace ProxyAgent {
23
25
  headers?: IncomingHttpHeaders;
24
26
  requestTls?: buildConnector.BuildOptions;
25
27
  proxyTls?: buildConnector.BuildOptions;
28
+ clientFactory?(origin: URL, opts: object): Dispatcher;
26
29
  }
27
30
  }
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
 
3
+ import type { MessagePort } from 'worker_threads'
3
4
  import {
4
5
  EventTarget,
5
6
  Event,