undici 5.19.1 → 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
@@ -405,6 +405,18 @@ implementations in Deno and Cloudflare Workers.
405
405
 
406
406
  Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
407
407
 
408
+ ## Workarounds
409
+
410
+ ### Network address family autoselection.
411
+
412
+ If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
413
+ first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
414
+ undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
415
+
416
+ If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
417
+ (18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
418
+ and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
419
+
408
420
  ## Collaborators
409
421
 
410
422
  * [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
@@ -17,8 +17,8 @@ Returns: `Client`
17
17
 
18
18
  ### Parameter: `ClientOptions`
19
19
 
20
- * **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
21
- * **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
20
+ * **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
21
+ * **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
22
22
  * **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
23
23
  * **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - 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. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
24
24
  * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second.
@@ -28,6 +28,8 @@ Returns: `Client`
28
28
  * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
29
29
  * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
30
30
  * **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time.
31
+ * **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
32
+ * **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
31
33
 
32
34
  #### Parameter: `ConnectOptions`
33
35
 
@@ -74,7 +74,7 @@ Returns: `void | Promise<ConnectData>` - Only returns a `Promise` if no `callbac
74
74
  #### Parameter: `ConnectData`
75
75
 
76
76
  * **statusCode** `number`
77
- * **headers** `Record<string, string | string[]>`
77
+ * **headers** `Record<string, string | string[] | undefined>`
78
78
  * **socket** `stream.Duplex`
79
79
  * **opaque** `unknown`
80
80
 
@@ -199,8 +199,8 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
199
199
  * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed.
200
200
  * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received.
201
201
  * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`.
202
- * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds.
203
- * **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds.
202
+ * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds.
203
+ * **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
204
204
  * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server.
205
205
 
206
206
  #### Parameter: `DispatchHandler`
@@ -383,7 +383,7 @@ Extends: [`RequestOptions`](#parameter-requestoptions)
383
383
  #### Parameter: PipelineHandlerData
384
384
 
385
385
  * **statusCode** `number`
386
- * **headers** `Record<string, string | string[]>`
386
+ * **headers** `Record<string, string | string[] | undefined>`
387
387
  * **opaque** `unknown`
388
388
  * **body** `stream.Readable`
389
389
  * **context** `object`
@@ -644,7 +644,7 @@ Returns: `void | Promise<StreamData>` - Only returns a `Promise` if no `callback
644
644
  #### Parameter: `StreamFactoryData`
645
645
 
646
646
  * **statusCode** `number`
647
- * **headers** `Record<string, string | string[]>`
647
+ * **headers** `Record<string, string | string[] | undefined>`
648
648
  * **opaque** `unknown`
649
649
  * **onInfo** `({statusCode: number, headers: Record<string, string | string[]>}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received.
650
650
 
@@ -853,9 +853,9 @@ Emitted when dispatcher is no longer busy.
853
853
 
854
854
  ## Parameter: `UndiciHeaders`
855
855
 
856
- * `Record<string, string | string[]> | string[] | null`
856
+ * `Record<string, string | string[] | undefined> | string[] | null`
857
857
 
858
- Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record<string, string | string[]>` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown.
858
+ Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record<string, string | string[] | undefined>` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown.
859
859
 
860
860
  Keys are lowercase and values are not modified.
861
861
 
@@ -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
 
package/index.js CHANGED
@@ -20,10 +20,6 @@ const DecoratorHandler = require('./lib/handler/DecoratorHandler')
20
20
  const RedirectHandler = require('./lib/handler/RedirectHandler')
21
21
  const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
22
22
 
23
- const nodeVersion = process.versions.node.split('.')
24
- const nodeMajor = Number(nodeVersion[0])
25
- const nodeMinor = Number(nodeVersion[1])
26
-
27
23
  let hasCrypto
28
24
  try {
29
25
  require('crypto')
@@ -100,7 +96,7 @@ function makeDispatcher (fn) {
100
96
  module.exports.setGlobalDispatcher = setGlobalDispatcher
101
97
  module.exports.getGlobalDispatcher = getGlobalDispatcher
102
98
 
103
- if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
99
+ if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) {
104
100
  let fetchImpl = null
105
101
  module.exports.fetch = async function fetch (resource) {
106
102
  if (!fetchImpl) {
@@ -127,7 +123,7 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) {
127
123
  module.exports.getGlobalOrigin = getGlobalOrigin
128
124
  }
129
125
 
130
- if (nodeMajor >= 16) {
126
+ if (util.nodeMajor >= 16) {
131
127
  const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies')
132
128
 
133
129
  module.exports.deleteCookie = deleteCookie
@@ -141,7 +137,7 @@ if (nodeMajor >= 16) {
141
137
  module.exports.serializeAMimeType = serializeAMimeType
142
138
  }
143
139
 
144
- if (nodeMajor >= 18 && hasCrypto) {
140
+ if (util.nodeMajor >= 18 && hasCrypto) {
145
141
  const { WebSocket } = require('./lib/websocket/websocket')
146
142
 
147
143
  module.exports.WebSocket = WebSocket
@@ -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,
@@ -109,7 +119,9 @@ class Client extends DispatcherBase {
109
119
  connect,
110
120
  maxRequestsPerClient,
111
121
  localAddress,
112
- maxResponseSize
122
+ maxResponseSize,
123
+ autoSelectFamily,
124
+ autoSelectFamilyAttemptTimeout
113
125
  } = {}) {
114
126
  super()
115
127
 
@@ -185,12 +197,20 @@ class Client extends DispatcherBase {
185
197
  throw new InvalidArgumentError('maxResponseSize must be a positive number')
186
198
  }
187
199
 
200
+ if (
201
+ autoSelectFamilyAttemptTimeout != null &&
202
+ (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)
203
+ ) {
204
+ throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number')
205
+ }
206
+
188
207
  if (typeof connect !== 'function') {
189
208
  connect = buildConnector({
190
209
  ...tls,
191
210
  maxCachedSessions,
192
211
  socketPath,
193
212
  timeout: connectTimeout,
213
+ ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
194
214
  ...connect
195
215
  })
196
216
  }
@@ -212,8 +232,8 @@ class Client extends DispatcherBase {
212
232
  this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming
213
233
  this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming
214
234
  this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n`
215
- this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 30e3
216
- this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 30e3
235
+ this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3
236
+ this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3
217
237
  this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
218
238
  this[kMaxRedirections] = maxRedirections
219
239
  this[kMaxRequests] = maxRequestsPerClient
@@ -1648,6 +1668,8 @@ class AsyncWriter {
1648
1668
  process.emitWarning(new RequestContentLengthMismatchError())
1649
1669
  }
1650
1670
 
1671
+ socket.cork()
1672
+
1651
1673
  if (bytesWritten === 0) {
1652
1674
  if (!expectsPayload) {
1653
1675
  socket[kReset] = true
@@ -1668,6 +1690,8 @@ class AsyncWriter {
1668
1690
 
1669
1691
  const ret = socket.write(chunk)
1670
1692
 
1693
+ socket.uncork()
1694
+
1671
1695
  request.onBodySent(chunk)
1672
1696
 
1673
1697
  if (!ret) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants')
4
4
  const { isCTLExcludingHtab } = require('./util')
5
- const { collectASequenceOfCodePoints } = require('../fetch/dataURL')
5
+ const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL')
6
6
  const assert = require('assert')
7
7
 
8
8
  /**
@@ -32,7 +32,7 @@ function parseSetCookie (header) {
32
32
  // (including the %x3B (";") in question).
33
33
  const position = { position: 0 }
34
34
 
35
- nameValuePair = collectASequenceOfCodePoints((char) => char !== ';', header, position)
35
+ nameValuePair = collectASequenceOfCodePointsFast(';', header, position)
36
36
  unparsedAttributes = header.slice(position.position)
37
37
  } else {
38
38
  // Otherwise:
@@ -54,8 +54,8 @@ function parseSetCookie (header) {
54
54
  // empty) value string consists of the characters after the first
55
55
  // %x3D ("=") character.
56
56
  const position = { position: 0 }
57
- name = collectASequenceOfCodePoints(
58
- (char) => char !== '=',
57
+ name = collectASequenceOfCodePointsFast(
58
+ '=',
59
59
  nameValuePair,
60
60
  position
61
61
  )
@@ -106,8 +106,8 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
106
106
  if (unparsedAttributes.includes(';')) {
107
107
  // 1. Consume the characters of the unparsed-attributes up to, but
108
108
  // not including, the first %x3B (";") character.
109
- cookieAv = collectASequenceOfCodePoints(
110
- (char) => char !== ';',
109
+ cookieAv = collectASequenceOfCodePointsFast(
110
+ ';',
111
111
  unparsedAttributes,
112
112
  { position: 0 }
113
113
  )
@@ -134,8 +134,8 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
134
134
  // character.
135
135
  const position = { position: 0 }
136
136
 
137
- attributeName = collectASequenceOfCodePoints(
138
- (char) => char !== '=',
137
+ attributeName = collectASequenceOfCodePointsFast(
138
+ '=',
139
139
  cookieAv,
140
140
  position
141
141
  )
@@ -34,10 +34,6 @@ const channels = {}
34
34
 
35
35
  let extractBody
36
36
 
37
- const nodeVersion = process.versions.node.split('.')
38
- const nodeMajor = Number(nodeVersion[0])
39
- const nodeMinor = Number(nodeVersion[1])
40
-
41
37
  try {
42
38
  const diagnosticsChannel = require('diagnostics_channel')
43
39
  channels.create = diagnosticsChannel.channel('undici:request:create')
@@ -172,7 +168,7 @@ class Request {
172
168
  }
173
169
 
174
170
  if (util.isFormDataLike(this.body)) {
175
- if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) {
171
+ if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
176
172
  throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
177
173
  }
178
174
 
package/lib/core/util.js CHANGED
@@ -10,10 +10,12 @@ const { Blob } = require('buffer')
10
10
  const nodeUtil = require('util')
11
11
  const { stringify } = require('querystring')
12
12
 
13
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
14
+
13
15
  function nop () {}
14
16
 
15
17
  function isStream (obj) {
16
- return obj && typeof obj.pipe === 'function'
18
+ return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
17
19
  }
18
20
 
19
21
  // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
@@ -44,6 +46,12 @@ function buildURL (url, queryParams) {
44
46
  function parseURL (url) {
45
47
  if (typeof url === 'string') {
46
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
47
55
  }
48
56
 
49
57
  if (!url || typeof url !== 'object') {
@@ -373,23 +381,34 @@ function ReadableStreamFrom (iterable) {
373
381
 
374
382
  // The chunk should be a FormData instance and contains
375
383
  // all the required methods.
376
- function isFormDataLike (chunk) {
377
- return (chunk &&
378
- chunk.constructor && chunk.constructor.name === 'FormData' &&
379
- typeof chunk === 'object' &&
380
- (typeof chunk.append === 'function' &&
381
- typeof chunk.delete === 'function' &&
382
- typeof chunk.get === 'function' &&
383
- typeof chunk.getAll === 'function' &&
384
- typeof chunk.has === 'function' &&
385
- typeof chunk.set === 'function' &&
386
- typeof chunk.entries === 'function' &&
387
- typeof chunk.keys === 'function' &&
388
- typeof chunk.values === 'function' &&
389
- 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'
390
395
  )
391
396
  }
392
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
+
393
412
  const kEnumerableProperty = Object.create(null)
394
413
  kEnumerableProperty.enumerable = true
395
414
 
@@ -420,5 +439,9 @@ module.exports = {
420
439
  validateHandler,
421
440
  getSocketInfo,
422
441
  isFormDataLike,
423
- buildURL
442
+ buildURL,
443
+ throwIfAborted,
444
+ nodeMajor,
445
+ nodeMinor,
446
+ nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
424
447
  }
@@ -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
@@ -556,6 +565,7 @@ module.exports = {
556
565
  dataURLProcessor,
557
566
  URLSerializer,
558
567
  collectASequenceOfCodePoints,
568
+ collectASequenceOfCodePointsFast,
559
569
  stringPercentDecode,
560
570
  parseMIMEType,
561
571
  collectAnHTTPQuotedString,
@@ -75,6 +75,7 @@ class HeadersList {
75
75
  if (init instanceof HeadersList) {
76
76
  this[kHeadersMap] = new Map(init[kHeadersMap])
77
77
  this[kHeadersSortedMap] = init[kHeadersSortedMap]
78
+ this.cookies = init.cookies
78
79
  } else {
79
80
  this[kHeadersMap] = new Map(init)
80
81
  this[kHeadersSortedMap] = null
@@ -53,7 +53,7 @@ const {
53
53
  const { kHeadersList } = require('../core/symbols')
54
54
  const EE = require('events')
55
55
  const { Readable, pipeline } = require('stream')
56
- const { isErrored, isReadable } = require('../core/util')
56
+ const { isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util')
57
57
  const { dataURLProcessor, serializeAMimeType } = require('./dataURL')
58
58
  const { TransformStream } = require('stream/web')
59
59
  const { getGlobalDispatcher } = require('../global')
@@ -64,10 +64,6 @@ const { STATUS_CODES } = require('http')
64
64
  let resolveObjectURL
65
65
  let ReadableStream = globalThis.ReadableStream
66
66
 
67
- const nodeVersion = process.versions.node.split('.')
68
- const nodeMajor = Number(nodeVersion[0])
69
- const nodeMinor = Number(nodeVersion[1])
70
-
71
67
  class Fetch extends EE {
72
68
  constructor (dispatcher) {
73
69
  super()
@@ -301,7 +297,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
301
297
  // capability.
302
298
  // TODO: given global’s relevant settings object’s cross-origin isolated
303
299
  // capability?
304
- response.timingInfo.endTime = coarsenedSharedCurrentTime()
300
+ timingInfo.endTime = coarsenedSharedCurrentTime()
305
301
 
306
302
  // 10. Set response’s timing info to timingInfo.
307
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 })