undici 5.12.0 → 5.14.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.
Files changed (45) hide show
  1. package/README.md +7 -5
  2. package/docs/api/Connector.md +3 -1
  3. package/docs/assets/lifecycle-diagram.png +0 -0
  4. package/index.d.ts +23 -23
  5. package/lib/core/connect.js +62 -19
  6. package/lib/fetch/body.js +56 -27
  7. package/lib/fetch/constants.js +19 -1
  8. package/lib/fetch/dataURL.js +5 -70
  9. package/lib/fetch/file.js +25 -46
  10. package/lib/fetch/formdata.js +30 -74
  11. package/lib/fetch/headers.js +52 -91
  12. package/lib/fetch/index.js +49 -43
  13. package/lib/fetch/request.js +35 -113
  14. package/lib/fetch/response.js +26 -54
  15. package/lib/fetch/symbols.js +2 -1
  16. package/lib/fetch/util.js +64 -21
  17. package/lib/fetch/webidl.js +38 -54
  18. package/lib/fileapi/filereader.js +47 -71
  19. package/lib/fileapi/progressevent.js +3 -9
  20. package/lib/fileapi/util.js +3 -9
  21. package/lib/mock/mock-utils.js +22 -4
  22. package/package.json +6 -6
  23. package/types/agent.d.ts +4 -5
  24. package/types/api.d.ts +1 -1
  25. package/types/balanced-pool.d.ts +3 -4
  26. package/types/client.d.ts +5 -5
  27. package/types/connector.d.ts +6 -4
  28. package/types/diagnostics-channel.d.ts +4 -4
  29. package/types/dispatcher.d.ts +23 -23
  30. package/types/errors.d.ts +3 -3
  31. package/types/fetch.d.ts +1 -1
  32. package/types/filereader.d.ts +8 -3
  33. package/types/global-dispatcher.d.ts +1 -1
  34. package/types/interceptors.d.ts +2 -2
  35. package/types/mock-agent.d.ts +3 -3
  36. package/types/mock-client.d.ts +4 -4
  37. package/types/mock-errors.d.ts +3 -3
  38. package/types/mock-interceptor.d.ts +1 -1
  39. package/types/mock-pool.d.ts +4 -4
  40. package/types/patch.d.ts +51 -0
  41. package/types/pool-stats.d.ts +2 -2
  42. package/types/pool.d.ts +5 -6
  43. package/types/proxy-agent.d.ts +3 -3
  44. package/types/readable.d.ts +1 -1
  45. package/types/webidl.d.ts +213 -0
package/README.md CHANGED
@@ -178,10 +178,6 @@ Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
178
178
 
179
179
  Only supported on Node 16.8+.
180
180
 
181
- This is [experimental](https://nodejs.org/api/documentation.html#documentation_stability_index) and is not yet fully compliant with the Fetch Standard.
182
- We plan to ship breaking changes to this feature until it is out of experimental.
183
- Help us improve the test coverage by following instructions at [nodejs/undici/#951](https://github.com/nodejs/undici/issues/951).
184
-
185
181
  Basic usage example:
186
182
 
187
183
  ```js
@@ -234,9 +230,15 @@ const data = {
234
230
  },
235
231
  }
236
232
 
237
- await fetch('https://example.com', { body: data, method: 'POST' })
233
+ await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
238
234
  ```
239
235
 
236
+ #### `request.duplex`
237
+
238
+ - half
239
+
240
+ In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`. And fetch requests are currently always be full duplex. More detail refer to [Fetch Standard.](https://fetch.spec.whatwg.org/#dom-requestinit-duplex)
241
+
240
242
  #### `response.body`
241
243
 
242
244
  Nodejs has two kinds of streams: [web streams](https://nodejs.org/dist/latest-v16.x/docs/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
@@ -24,8 +24,10 @@ Once you call `buildConnector`, it will return a connector function, which takes
24
24
  * **hostname** `string` (required)
25
25
  * **host** `string` (optional)
26
26
  * **protocol** `string` (required)
27
- * **port** `number` (required)
27
+ * **port** `string` (required)
28
28
  * **servername** `string` (optional)
29
+ * **localAddress** `string | null` (optional) Local address the socket should connect from.
30
+ * **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.
29
31
 
30
32
  ### Basic example
31
33
 
Binary file
package/index.d.ts CHANGED
@@ -1,19 +1,19 @@
1
- import Dispatcher = require('./types/dispatcher')
1
+ import Dispatcher from'./types/dispatcher'
2
2
  import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
3
3
  import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
4
- import Pool = require('./types/pool')
4
+ import Pool from'./types/pool'
5
5
  import { RedirectHandler, DecoratorHandler } from './types/handlers'
6
6
 
7
- import BalancedPool = require('./types/balanced-pool')
8
- import Client = require('./types/client')
9
- import buildConnector = require('./types/connector')
10
- import errors = require('./types/errors')
11
- import Agent = require('./types/agent')
12
- import MockClient = require('./types/mock-client')
13
- import MockPool = require('./types/mock-pool')
14
- import MockAgent = require('./types/mock-agent')
15
- import mockErrors = require('./types/mock-errors')
16
- import ProxyAgent = require('./types/proxy-agent')
7
+ import BalancedPool from './types/balanced-pool'
8
+ import Client from'./types/client'
9
+ import buildConnector from'./types/connector'
10
+ import errors from'./types/errors'
11
+ import Agent from'./types/agent'
12
+ import MockClient from'./types/mock-client'
13
+ import MockPool from'./types/mock-pool'
14
+ import MockAgent from'./types/mock-agent'
15
+ import mockErrors from'./types/mock-errors'
16
+ import ProxyAgent from'./types/proxy-agent'
17
17
  import { request, pipeline, stream, connect, upgrade } from './types/api'
18
18
 
19
19
  export * from './types/fetch'
@@ -27,16 +27,16 @@ export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent,
27
27
  export default Undici
28
28
 
29
29
  declare namespace Undici {
30
- var Dispatcher: typeof import('./types/dispatcher')
31
- var Pool: typeof import('./types/pool');
30
+ var Dispatcher: typeof import('./types/dispatcher').default
31
+ var Pool: typeof import('./types/pool').default;
32
32
  var RedirectHandler: typeof import ('./types/handlers').RedirectHandler
33
33
  var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler
34
34
  var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor
35
- var BalancedPool: typeof import('./types/balanced-pool');
36
- var Client: typeof import('./types/client');
37
- var buildConnector: typeof import('./types/connector');
38
- var errors: typeof import('./types/errors');
39
- var Agent: typeof import('./types/agent');
35
+ var BalancedPool: typeof import('./types/balanced-pool').default;
36
+ var Client: typeof import('./types/client').default;
37
+ var buildConnector: typeof import('./types/connector').default;
38
+ var errors: typeof import('./types/errors').default;
39
+ var Agent: typeof import('./types/agent').default;
40
40
  var setGlobalDispatcher: typeof import('./types/global-dispatcher').setGlobalDispatcher;
41
41
  var getGlobalDispatcher: typeof import('./types/global-dispatcher').getGlobalDispatcher;
42
42
  var request: typeof import('./types/api').request;
@@ -44,9 +44,9 @@ declare namespace Undici {
44
44
  var pipeline: typeof import('./types/api').pipeline;
45
45
  var connect: typeof import('./types/api').connect;
46
46
  var upgrade: typeof import('./types/api').upgrade;
47
- var MockClient: typeof import('./types/mock-client');
48
- var MockPool: typeof import('./types/mock-pool');
49
- var MockAgent: typeof import('./types/mock-agent');
50
- var mockErrors: typeof import('./types/mock-errors');
47
+ var MockClient: typeof import('./types/mock-client').default;
48
+ var MockPool: typeof import('./types/mock-pool').default;
49
+ var MockAgent: typeof import('./types/mock-agent').default;
50
+ var mockErrors: typeof import('./types/mock-errors').default;
51
51
  var fetch: typeof import('./types/fetch').fetch;
52
52
  }
@@ -4,6 +4,7 @@ const net = require('net')
4
4
  const assert = require('assert')
5
5
  const util = require('./util')
6
6
  const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
7
+
7
8
  let tls // include tls conditionally since it is not always available
8
9
 
9
10
  // TODO: session re-use does not wait for the first
@@ -11,15 +12,73 @@ let tls // include tls conditionally since it is not always available
11
12
  // resolve the same servername multiple times even when
12
13
  // re-use is enabled.
13
14
 
15
+ let SessionCache
16
+ if (global.FinalizationRegistry) {
17
+ SessionCache = class WeakSessionCache {
18
+ constructor (maxCachedSessions) {
19
+ this._maxCachedSessions = maxCachedSessions
20
+ this._sessionCache = new Map()
21
+ this._sessionRegistry = new global.FinalizationRegistry((key) => {
22
+ if (this._sessionCache.size < this._maxCachedSessions) {
23
+ return
24
+ }
25
+
26
+ const ref = this._sessionCache.get(key)
27
+ if (ref !== undefined && ref.deref() === undefined) {
28
+ this._sessionCache.delete(key)
29
+ }
30
+ })
31
+ }
32
+
33
+ get (sessionKey) {
34
+ const ref = this._sessionCache.get(sessionKey)
35
+ return ref ? ref.deref() : null
36
+ }
37
+
38
+ set (sessionKey, session) {
39
+ if (this._maxCachedSessions === 0) {
40
+ return
41
+ }
42
+
43
+ this._sessionCache.set(sessionKey, new WeakRef(session))
44
+ this._sessionRegistry.register(session, sessionKey)
45
+ }
46
+ }
47
+ } else {
48
+ SessionCache = class SimpleSessionCache {
49
+ constructor (maxCachedSessions) {
50
+ this._maxCachedSessions = maxCachedSessions
51
+ this._sessionCache = new Map()
52
+ }
53
+
54
+ get (sessionKey) {
55
+ return this._sessionCache.get(sessionKey)
56
+ }
57
+
58
+ set (sessionKey, session) {
59
+ if (this._maxCachedSessions === 0) {
60
+ return
61
+ }
62
+
63
+ if (this._sessionCache.size >= this._maxCachedSessions) {
64
+ // remove the oldest session
65
+ const { value: oldestKey } = this._sessionCache.keys().next()
66
+ this._sessionCache.delete(oldestKey)
67
+ }
68
+
69
+ this._sessionCache.set(sessionKey, session)
70
+ }
71
+ }
72
+ }
73
+
14
74
  function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
15
75
  if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
16
76
  throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
17
77
  }
18
78
 
19
79
  const options = { path: socketPath, ...opts }
20
- const sessionCache = new Map()
80
+ const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
21
81
  timeout = timeout == null ? 10e3 : timeout
22
- maxCachedSessions = maxCachedSessions == null ? 100 : maxCachedSessions
23
82
 
24
83
  return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
25
84
  let socket
@@ -47,25 +106,9 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
47
106
 
48
107
  socket
49
108
  .on('session', function (session) {
50
- // cache is disabled
51
- if (maxCachedSessions === 0) {
52
- return
53
- }
54
-
55
- if (sessionCache.size >= maxCachedSessions) {
56
- // remove the oldest session
57
- const { value: oldestKey } = sessionCache.keys().next()
58
- sessionCache.delete(oldestKey)
59
- }
60
-
109
+ // TODO (fix): Can a session become invalid once established? Don't think so?
61
110
  sessionCache.set(sessionKey, session)
62
111
  })
63
- .on('error', function (err) {
64
- if (sessionKey && err.code !== 'UND_ERR_INFO') {
65
- // TODO (fix): Only delete for session related errors.
66
- sessionCache.delete(sessionKey)
67
- }
68
- })
69
112
  } else {
70
113
  assert(!httpSocket, 'httpSocket can only be sent on TLS update')
71
114
  socket = net.connect({
package/lib/fetch/body.js CHANGED
@@ -2,22 +2,24 @@
2
2
 
3
3
  const Busboy = require('busboy')
4
4
  const util = require('../core/util')
5
- const { ReadableStreamFrom, toUSVString, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
5
+ const { ReadableStreamFrom, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
6
6
  const { FormData } = require('./formdata')
7
7
  const { kState } = require('./symbols')
8
8
  const { webidl } = require('./webidl')
9
9
  const { DOMException, structuredClone } = require('./constants')
10
- const { Blob } = require('buffer')
10
+ const { Blob, File: NativeFile } = require('buffer')
11
11
  const { kBodyUsed } = require('../core/symbols')
12
12
  const assert = require('assert')
13
13
  const { isErrored } = require('../core/util')
14
14
  const { isUint8Array, isArrayBuffer } = require('util/types')
15
- const { File } = require('./file')
15
+ const { File: UndiciFile } = require('./file')
16
16
  const { StringDecoder } = require('string_decoder')
17
17
  const { parseMIMEType, serializeAMimeType } = require('./dataURL')
18
18
 
19
- /** @type {globalThis['ReadableStream']} */
20
- let ReadableStream
19
+ let ReadableStream = globalThis.ReadableStream
20
+
21
+ /** @type {globalThis['File']} */
22
+ const File = NativeFile ?? UndiciFile
21
23
 
22
24
  // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
23
25
  function extractBody (object, keepalive = false) {
@@ -66,9 +68,13 @@ function extractBody (object, keepalive = false) {
66
68
  let type = null
67
69
 
68
70
  // 10. Switch on object:
69
- if (object == null) {
70
- // Note: The IDL processor cannot handle this situation. See
71
- // https://crbug.com/335871.
71
+ if (typeof object === 'string') {
72
+ // Set source to the UTF-8 encoding of object.
73
+ // Note: setting source to a Uint8Array here breaks some mocking assumptions.
74
+ source = object
75
+
76
+ // Set type to `text/plain;charset=UTF-8`.
77
+ type = 'text/plain;charset=UTF-8'
72
78
  } else if (object instanceof URLSearchParams) {
73
79
  // URLSearchParams
74
80
 
@@ -126,7 +132,8 @@ function extractBody (object, keepalive = false) {
126
132
 
127
133
  yield * value.stream()
128
134
 
129
- yield enc.encode('\r\n')
135
+ // '\r\n' encoded
136
+ yield new Uint8Array([13, 10])
130
137
  }
131
138
  }
132
139
 
@@ -137,7 +144,33 @@ function extractBody (object, keepalive = false) {
137
144
  source = object
138
145
 
139
146
  // Set length to unclear, see html/6424 for improving this.
140
- // TODO
147
+ length = (() => {
148
+ const prefixLength = prefix.length
149
+ const boundaryLength = boundary.length
150
+ let bodyLength = 0
151
+
152
+ for (const [name, value] of object) {
153
+ if (typeof value === 'string') {
154
+ bodyLength +=
155
+ prefixLength +
156
+ Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
157
+ } else {
158
+ bodyLength +=
159
+ prefixLength +
160
+ Buffer.byteLength(`; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '')) +
161
+ 2 + // \r\n
162
+ `Content-Type: ${
163
+ value.type || 'application/octet-stream'
164
+ }\r\n\r\n`.length
165
+
166
+ // value is a Blob or File, and \r\n
167
+ bodyLength += value.size + 2
168
+ }
169
+ }
170
+
171
+ bodyLength += boundaryLength + 4 // --boundary--
172
+ return bodyLength
173
+ })()
141
174
 
142
175
  // Set type to `multipart/form-data; boundary=`,
143
176
  // followed by the multipart/form-data boundary string generated
@@ -157,6 +190,11 @@ function extractBody (object, keepalive = false) {
157
190
  if (object.type) {
158
191
  type = object.type
159
192
  }
193
+ } else if (object instanceof Uint8Array) {
194
+ // byte sequence
195
+
196
+ // Set source to object.
197
+ source = object
160
198
  } else if (typeof object[Symbol.asyncIterator] === 'function') {
161
199
  // If keepalive is true, then throw a TypeError.
162
200
  if (keepalive) {
@@ -172,17 +210,10 @@ function extractBody (object, keepalive = false) {
172
210
 
173
211
  stream =
174
212
  object instanceof ReadableStream ? object : ReadableStreamFrom(object)
175
- } else {
176
- // TODO: byte sequence?
177
- // TODO: scalar value string?
178
- // TODO: else?
179
- source = toUSVString(object)
180
- type = 'text/plain;charset=UTF-8'
181
213
  }
182
214
 
183
215
  // 11. If source is a byte sequence, then set action to a
184
216
  // step that returns source and length to source’s length.
185
- // TODO: What is a "byte sequence?"
186
217
  if (typeof source === 'string' || util.isBuffer(source)) {
187
218
  length = Buffer.byteLength(source)
188
219
  }
@@ -329,9 +360,7 @@ function bodyMixinMethods (instance) {
329
360
  },
330
361
 
331
362
  async formData () {
332
- if (!(this instanceof instance)) {
333
- throw new TypeError('Illegal invocation')
334
- }
363
+ webidl.brandCheck(this, instance)
335
364
 
336
365
  throwIfAborted(this[kState])
337
366
 
@@ -347,7 +376,10 @@ function bodyMixinMethods (instance) {
347
376
  let busboy
348
377
 
349
378
  try {
350
- busboy = Busboy({ headers })
379
+ busboy = Busboy({
380
+ headers,
381
+ defParamCharset: 'utf8'
382
+ })
351
383
  } catch (err) {
352
384
  // Error due to headers:
353
385
  throw Object.assign(new TypeError(), { cause: err })
@@ -360,7 +392,7 @@ function bodyMixinMethods (instance) {
360
392
  const { filename, encoding, mimeType } = info
361
393
  const chunks = []
362
394
 
363
- if (encoding.toLowerCase() === 'base64') {
395
+ if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
364
396
  let base64chunk = ''
365
397
 
366
398
  value.on('data', (chunk) => {
@@ -433,7 +465,7 @@ function bodyMixinMethods (instance) {
433
465
  throwIfAborted(this[kState])
434
466
 
435
467
  // Otherwise, throw a TypeError.
436
- webidl.errors.exception({
468
+ throw webidl.errors.exception({
437
469
  header: `${instance.name}.formData`,
438
470
  message: 'Could not parse content as FormData.'
439
471
  })
@@ -450,11 +482,8 @@ function mixinBody (prototype) {
450
482
 
451
483
  // https://fetch.spec.whatwg.org/#concept-body-consume-body
452
484
  async function specConsumeBody (object, type, instance) {
453
- if (!(object instanceof instance)) {
454
- throw new TypeError('Illegal invocation')
455
- }
485
+ webidl.brandCheck(object, instance)
456
486
 
457
- // TODO: why is this needed?
458
487
  throwIfAborted(object[kState])
459
488
 
460
489
  // 1. If object is unusable, then return a promise rejected
@@ -8,6 +8,17 @@ const nullBodyStatus = [101, 204, 205, 304]
8
8
 
9
9
  const redirectStatus = [301, 302, 303, 307, 308]
10
10
 
11
+ // https://fetch.spec.whatwg.org/#block-bad-port
12
+ const badPorts = [
13
+ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
14
+ '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
15
+ '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
16
+ '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
17
+ '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
18
+ '10080'
19
+ ]
20
+
21
+ // https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
11
22
  const referrerPolicy = [
12
23
  '',
13
24
  'no-referrer',
@@ -44,6 +55,11 @@ const requestBodyHeader = [
44
55
  'content-type'
45
56
  ]
46
57
 
58
+ // https://fetch.spec.whatwg.org/#enumdef-requestduplex
59
+ const requestDuplex = [
60
+ 'half'
61
+ ]
62
+
47
63
  // http://fetch.spec.whatwg.org/#forbidden-method
48
64
  const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
49
65
 
@@ -108,5 +124,7 @@ module.exports = {
108
124
  redirectStatus,
109
125
  corsSafeListedMethods,
110
126
  nullBodyStatus,
111
- safeMethods
127
+ safeMethods,
128
+ badPorts,
129
+ requestDuplex
112
130
  }
@@ -1,6 +1,7 @@
1
1
  const assert = require('assert')
2
2
  const { atob } = require('buffer')
3
- const { isValidHTTPToken } = require('./util')
3
+ const { format } = require('url')
4
+ const { isValidHTTPToken, isomorphicDecode } = require('./util')
4
5
 
5
6
  const encoder = new TextEncoder()
6
7
 
@@ -54,7 +55,6 @@ function dataURLProcessor (dataURL) {
54
55
  const encodedBody = input.slice(mimeTypeLength + 1)
55
56
 
56
57
  // 10. Let body be the percent-decoding of encodedBody.
57
- /** @type {Uint8Array|string} */
58
58
  let body = stringPercentDecode(encodedBody)
59
59
 
60
60
  // 11. If mimeType ends with U+003B (;), followed by
@@ -62,7 +62,8 @@ function dataURLProcessor (dataURL) {
62
62
  // case-insensitive match for "base64", then:
63
63
  if (/;(\u0020){0,}base64$/i.test(mimeType)) {
64
64
  // 1. Let stringBody be the isomorphic decode of body.
65
- const stringBody = decodeURIComponent(new TextDecoder('utf-8').decode(body))
65
+ const stringBody = isomorphicDecode(body)
66
+
66
67
  // 2. Set body to the forgiving-base64 decode of
67
68
  // stringBody.
68
69
  body = forgivingBase64(stringBody)
@@ -111,73 +112,7 @@ function dataURLProcessor (dataURL) {
111
112
  * @param {boolean} excludeFragment
112
113
  */
113
114
  function URLSerializer (url, excludeFragment = false) {
114
- // 1. Let output be url’s scheme and U+003A (:) concatenated.
115
- let output = url.protocol
116
-
117
- // 2. If url’s host is non-null:
118
- if (url.host.length > 0) {
119
- // 1. Append "//" to output.
120
- output += '//'
121
-
122
- // 2. If url includes credentials, then:
123
- if (url.username.length > 0 || url.password.length > 0) {
124
- // 1. Append url’s username to output.
125
- output += url.username
126
-
127
- // 2. If url’s password is not the empty string, then append U+003A (:),
128
- // followed by url’s password, to output.
129
- if (url.password.length > 0) {
130
- output += ':' + url.password
131
- }
132
-
133
- // 3. Append U+0040 (@) to output.
134
- output += '@'
135
- }
136
-
137
- // 3. Append url’s host, serialized, to output.
138
- output += decodeURIComponent(url.hostname)
139
-
140
- // 4. If url’s port is non-null, append U+003A (:) followed by url’s port,
141
- // serialized, to output.
142
- if (url.port.length > 0) {
143
- output += ':' + url.port
144
- }
145
- }
146
-
147
- // 3. If url’s host is null, url does not have an opaque path,
148
- // url’s path’s size is greater than 1, and url’s path[0]
149
- // is the empty string, then append U+002F (/) followed by
150
- // U+002E (.) to output.
151
- // Note: This prevents web+demo:/.//not-a-host/ or web+demo:/path/..//not-a-host/,
152
- // when parsed and then serialized, from ending up as web+demo://not-a-host/
153
- // (they end up as web+demo:/.//not-a-host/).
154
- // Undici implementation note: url's path[0] can never be an
155
- // empty string, so we have to slightly alter what the spec says.
156
- if (
157
- url.host.length === 0 &&
158
- url.pathname.length > 1 &&
159
- url.href.slice(url.protocol.length + 1)[0] === '.'
160
- ) {
161
- output += '/.'
162
- }
163
-
164
- // 4. Append the result of URL path serializing url to output.
165
- output += url.pathname
166
-
167
- // 5. If url’s query is non-null, append U+003F (?),
168
- // followed by url’s query, to output.
169
- if (url.search.length > 0) {
170
- output += url.search
171
- }
172
-
173
- // 6. If exclude fragment is false and url’s fragment is non-null,
174
- // then append U+0023 (#), followed by url’s fragment, to output.
175
- if (excludeFragment === false && url.hash.length > 0) {
176
- output += url.hash
177
- }
178
-
179
- // 7. Return output.
180
- return output
115
+ return format(url, { fragment: !excludeFragment })
181
116
  }
182
117
 
183
118
  // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points