undici 5.20.0 → 5.21.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
@@ -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) {
@@ -41,7 +41,7 @@ module.exports = {
41
41
  kClient: Symbol('client'),
42
42
  kParser: Symbol('parser'),
43
43
  kOnDestroyed: Symbol('destroy callbacks'),
44
- kPipelining: Symbol('pipelinig'),
44
+ kPipelining: Symbol('pipelining'),
45
45
  kSocket: Symbol('socket'),
46
46
  kHostHeader: Symbol('host header'),
47
47
  kConnector: Symbol('connector'),
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,34 +46,40 @@ 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 URL protocol: the URL must start with `http:` or `https:`.')
52
+ }
53
+
54
+ return url
49
55
  }
50
56
 
51
57
  if (!url || typeof url !== 'object') {
52
- throw new InvalidArgumentError('invalid url')
58
+ throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
53
59
  }
54
60
 
55
61
  if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
56
- throw new InvalidArgumentError('invalid port')
62
+ throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
57
63
  }
58
64
 
59
65
  if (url.path != null && typeof url.path !== 'string') {
60
- throw new InvalidArgumentError('invalid path')
66
+ throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
61
67
  }
62
68
 
63
69
  if (url.pathname != null && typeof url.pathname !== 'string') {
64
- throw new InvalidArgumentError('invalid pathname')
70
+ throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
65
71
  }
66
72
 
67
73
  if (url.hostname != null && typeof url.hostname !== 'string') {
68
- throw new InvalidArgumentError('invalid hostname')
74
+ throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
69
75
  }
70
76
 
71
77
  if (url.origin != null && typeof url.origin !== 'string') {
72
- throw new InvalidArgumentError('invalid origin')
78
+ throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
73
79
  }
74
80
 
75
81
  if (!/^https?:/.test(url.origin || url.protocol)) {
76
- throw new InvalidArgumentError('invalid protocol')
82
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
77
83
  }
78
84
 
79
85
  if (!(url instanceof URL)) {
@@ -375,23 +381,49 @@ 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
+
412
+ const hasToWellFormed = !!String.prototype.toWellFormed
413
+
414
+ /**
415
+ * @param {string} val
416
+ */
417
+ function toUSVString (val) {
418
+ if (hasToWellFormed) {
419
+ return `${val}`.toWellFormed()
420
+ } else if (nodeUtil.toUSVString) {
421
+ return nodeUtil.toUSVString(val)
422
+ }
423
+
424
+ return `${val}`
425
+ }
426
+
395
427
  const kEnumerableProperty = Object.create(null)
396
428
  kEnumerableProperty.enumerable = true
397
429
 
@@ -401,7 +433,7 @@ module.exports = {
401
433
  isDisturbed,
402
434
  isErrored,
403
435
  isReadable,
404
- toUSVString: nodeUtil.toUSVString || ((val) => `${val}`),
436
+ toUSVString,
405
437
  isReadableAborted,
406
438
  isBlobLike,
407
439
  parseOrigin,
@@ -423,6 +455,7 @@ module.exports = {
423
455
  getSocketInfo,
424
456
  isFormDataLike,
425
457
  buildURL,
458
+ throwIfAborted,
426
459
  nodeMajor,
427
460
  nodeMinor,
428
461
  nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
@@ -48,11 +48,17 @@ const requestCache = [
48
48
  'only-if-cached'
49
49
  ]
50
50
 
51
+ // https://fetch.spec.whatwg.org/#request-body-header-name
51
52
  const requestBodyHeader = [
52
53
  'content-encoding',
53
54
  'content-language',
54
55
  'content-location',
55
- 'content-type'
56
+ 'content-type',
57
+ // See https://github.com/nodejs/undici/issues/2021
58
+ // 'Content-Length' is a forbidden header name, which is typically
59
+ // removed in the Headers implementation. However, undici doesn't
60
+ // filter out headers, so we add it here.
61
+ 'content-length'
56
62
  ]
57
63
 
58
64
  // https://fetch.spec.whatwg.org/#enumdef-requestduplex
@@ -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
@@ -61,14 +61,7 @@ class FormData {
61
61
 
62
62
  // The delete(name) method steps are to remove all entries whose name
63
63
  // is name from this’s entry list.
64
- const next = []
65
- for (const entry of this[kState]) {
66
- if (entry.name !== name) {
67
- next.push(entry)
68
- }
69
- }
70
-
71
- this[kState] = next
64
+ this[kState] = this[kState].filter(entry => entry.name !== name)
72
65
  }
73
66
 
74
67
  get (name) {
@@ -37,7 +37,10 @@ const {
37
37
  isErrorLike,
38
38
  fullyReadBody,
39
39
  readableStreamClose,
40
- isomorphicEncode
40
+ isomorphicEncode,
41
+ urlIsLocal,
42
+ urlIsHttpHttpsScheme,
43
+ urlHasHttpsScheme
41
44
  } = require('./util')
42
45
  const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols')
43
46
  const assert = require('assert')
@@ -272,7 +275,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
272
275
  let cacheState = response.cacheState
273
276
 
274
277
  // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
275
- if (!/^https?:/.test(originalURL.protocol)) {
278
+ if (!urlIsHttpHttpsScheme(originalURL)) {
276
279
  return
277
280
  }
278
281
 
@@ -297,7 +300,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
297
300
  // capability.
298
301
  // TODO: given global’s relevant settings object’s cross-origin isolated
299
302
  // capability?
300
- response.timingInfo.endTime = coarsenedSharedCurrentTime()
303
+ timingInfo.endTime = coarsenedSharedCurrentTime()
301
304
 
302
305
  // 10. Set response’s timing info to timingInfo.
303
306
  response.timingInfo = timingInfo
@@ -530,10 +533,7 @@ async function mainFetch (fetchParams, recursive = false) {
530
533
 
531
534
  // 3. If request’s local-URLs-only flag is set and request’s current URL is
532
535
  // not local, then set response to a network error.
533
- if (
534
- request.localURLsOnly &&
535
- !/^(about|blob|data):/.test(requestCurrentURL(request).protocol)
536
- ) {
536
+ if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
537
537
  response = makeNetworkError('local URLs only')
538
538
  }
539
539
 
@@ -623,7 +623,7 @@ async function mainFetch (fetchParams, recursive = false) {
623
623
  }
624
624
 
625
625
  // request’s current URL’s scheme is not an HTTP(S) scheme
626
- if (!/^https?:/.test(requestCurrentURL(request).protocol)) {
626
+ if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
627
627
  // Return a network error.
628
628
  return makeNetworkError('URL scheme must be a HTTP(S) scheme')
629
629
  }
@@ -1130,7 +1130,7 @@ async function httpRedirectFetch (fetchParams, response) {
1130
1130
 
1131
1131
  // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
1132
1132
  // error.
1133
- if (!/^https?:/.test(locationURL.protocol)) {
1133
+ if (!urlIsHttpHttpsScheme(locationURL)) {
1134
1134
  return makeNetworkError('URL scheme must be a HTTP(S) scheme')
1135
1135
  }
1136
1136
 
@@ -1205,7 +1205,7 @@ async function httpRedirectFetch (fetchParams, response) {
1205
1205
  // 14. If request’s body is non-null, then set request’s body to the first return
1206
1206
  // value of safely extracting request’s body’s source.
1207
1207
  if (request.body != null) {
1208
- assert(request.body.source)
1208
+ assert(request.body.source != null)
1209
1209
  request.body = safelyExtractBody(request.body.source)[0]
1210
1210
  }
1211
1211
 
@@ -1399,7 +1399,7 @@ async function httpNetworkOrCacheFetch (
1399
1399
  // header if httpRequest’s header list contains that header’s name.
1400
1400
  // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
1401
1401
  if (!httpRequest.headersList.contains('accept-encoding')) {
1402
- if (/^https:/.test(requestCurrentURL(httpRequest).protocol)) {
1402
+ if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
1403
1403
  httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate')
1404
1404
  } else {
1405
1405
  httpRequest.headersList.append('accept-encoding', 'gzip, deflate')
@@ -1845,6 +1845,7 @@ async function httpNetworkFetch (
1845
1845
  // 4. Set bytes to the result of handling content codings given
1846
1846
  // codings and bytes.
1847
1847
  let bytes
1848
+ let isFailure
1848
1849
  try {
1849
1850
  const { done, value } = await fetchParams.controller.next()
1850
1851
 
@@ -1859,6 +1860,10 @@ async function httpNetworkFetch (
1859
1860
  bytes = undefined
1860
1861
  } else {
1861
1862
  bytes = err
1863
+
1864
+ // err may be propagated from the result of calling readablestream.cancel,
1865
+ // which might not be an error. https://github.com/nodejs/undici/issues/2009
1866
+ isFailure = true
1862
1867
  }
1863
1868
  }
1864
1869
 
@@ -1878,7 +1883,7 @@ async function httpNetworkFetch (
1878
1883
  timingInfo.decodedBodySize += bytes?.byteLength ?? 0
1879
1884
 
1880
1885
  // 6. If bytes is failure, then terminate fetchParams’s controller.
1881
- if (isErrorLike(bytes)) {
1886
+ if (isFailure) {
1882
1887
  fetchParams.controller.terminate(bytes)
1883
1888
  return
1884
1889
  }
@@ -1979,7 +1984,9 @@ async function httpNetworkFetch (
1979
1984
  const val = headersList[n + 1].toString('latin1')
1980
1985
 
1981
1986
  if (key.toLowerCase() === 'content-encoding') {
1982
- codings = val.split(',').map((x) => x.trim())
1987
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
1988
+ // "All content-coding values are case-insensitive..."
1989
+ codings = val.toLowerCase().split(',').map((x) => x.trim())
1983
1990
  } else if (key.toLowerCase() === 'location') {
1984
1991
  location = val
1985
1992
  }
@@ -1998,9 +2005,10 @@ async function httpNetworkFetch (
1998
2005
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
1999
2006
  if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
2000
2007
  for (const coding of codings) {
2001
- if (/(x-)?gzip/.test(coding)) {
2008
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
2009
+ if (coding === 'x-gzip' || coding === 'gzip') {
2002
2010
  decoders.push(zlib.createGunzip())
2003
- } else if (/(x-)?deflate/.test(coding)) {
2011
+ } else if (coding === 'deflate') {
2004
2012
  decoders.push(zlib.createInflate())
2005
2013
  } else if (coding === 'br') {
2006
2014
  decoders.push(zlib.createBrotliDecompress())
@@ -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,
@@ -33,6 +34,7 @@ const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('eve
33
34
  let TransformStream = globalThis.TransformStream
34
35
 
35
36
  const kInit = Symbol('init')
37
+ const kAbortController = Symbol('abortController')
36
38
 
37
39
  const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
38
40
  signal.removeEventListener('abort', abort)
@@ -51,10 +53,14 @@ class Request {
51
53
  input = webidl.converters.RequestInfo(input)
52
54
  init = webidl.converters.RequestInit(init)
53
55
 
54
- // TODO
56
+ // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
55
57
  this[kRealm] = {
56
58
  settingsObject: {
57
- baseUrl: getGlobalOrigin()
59
+ baseUrl: getGlobalOrigin(),
60
+ get origin () {
61
+ return this.baseUrl?.origin
62
+ },
63
+ policyContainer: makePolicyContainer()
58
64
  }
59
65
  }
60
66
 
@@ -123,12 +129,12 @@ class Request {
123
129
  }
124
130
 
125
131
  // 10. If init["window"] exists and is non-null, then throw a TypeError.
126
- if (init.window !== undefined && init.window != null) {
132
+ if (init.window != null) {
127
133
  throw new TypeError(`'window' option '${window}' must be null`)
128
134
  }
129
135
 
130
136
  // 11. If init["window"] exists, then set window to "no-window".
131
- if (init.window !== undefined) {
137
+ if ('window' in init) {
132
138
  window = 'no-window'
133
139
  }
134
140
 
@@ -349,17 +355,30 @@ class Request {
349
355
  if (signal.aborted) {
350
356
  ac.abort(signal.reason)
351
357
  } else {
358
+ // Keep a strong ref to ac while request object
359
+ // is alive. This is needed to prevent AbortController
360
+ // from being prematurely garbage collected.
361
+ // See, https://github.com/nodejs/undici/issues/1926.
362
+ this[kAbortController] = ac
363
+
352
364
  const acRef = new WeakRef(ac)
353
365
  const abort = function () {
354
- acRef.deref()?.abort(this.reason)
366
+ const ac = acRef.deref()
367
+ if (ac !== undefined) {
368
+ ac.abort(this.reason)
369
+ }
355
370
  }
356
371
 
357
- if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
358
- setMaxListeners(100, signal)
359
- }
372
+ // Third-party AbortControllers may not work with these.
373
+ // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619.
374
+ try {
375
+ if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) {
376
+ setMaxListeners(100, signal)
377
+ }
378
+ } catch {}
360
379
 
361
380
  signal.addEventListener('abort', abort, { once: true })
362
- requestFinalizer.register(this, { signal, abort })
381
+ requestFinalizer.register(ac, { signal, abort })
363
382
  }
364
383
  }
365
384
 
@@ -419,7 +438,7 @@ class Request {
419
438
  // non-null, and request’s method is `GET` or `HEAD`, then throw a
420
439
  // TypeError.
421
440
  if (
422
- ((init.body !== undefined && init.body != null) || inputBody != null) &&
441
+ (init.body != null || inputBody != null) &&
423
442
  (request.method === 'GET' || request.method === 'HEAD')
424
443
  ) {
425
444
  throw new TypeError('Request with GET/HEAD method cannot have body.')
@@ -429,7 +448,7 @@ class Request {
429
448
  let initBody = null
430
449
 
431
450
  // 36. If init["body"] exists and is non-null, then:
432
- if (init.body !== undefined && init.body != null) {
451
+ if (init.body != null) {
433
452
  // 1. Let Content-Type be null.
434
453
  // 2. Set initBody and Content-Type to the result of extracting
435
454
  // init["body"], with keepalive set to request’s keepalive.
@@ -348,9 +348,7 @@ function makeNetworkError (reason) {
348
348
  status: 0,
349
349
  error: isError
350
350
  ? reason
351
- : new Error(reason ? String(reason) : reason, {
352
- cause: isError ? reason : undefined
353
- }),
351
+ : new Error(reason ? String(reason) : reason),
354
352
  aborted: reason && reason.name === 'AbortError'
355
353
  })
356
354
  }