@florianpat/lando-core 3.23.22 → 3.23.27-2florianPat.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 (54) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/app.js +5 -0
  3. package/builders/_lando.js +19 -6
  4. package/builders/lando-v4.js +3 -0
  5. package/config.yml +4 -4
  6. package/hooks/app-add-init-tooling.js +21 -0
  7. package/hooks/app-run-events.js +22 -0
  8. package/hooks/lando-setup-build-engine-darwin.js +3 -0
  9. package/hooks/lando-setup-build-engine-win32.js +2 -0
  10. package/hooks/lando-setup-build-engine-wsl.js +2 -0
  11. package/hooks/lando-setup-orchestrator.js +2 -2
  12. package/lib/app.js +30 -24
  13. package/lib/daemon.js +1 -1
  14. package/lib/engine.js +1 -1
  15. package/lib/formatters.js +1 -1
  16. package/lib/router.js +1 -1
  17. package/lib/updates.js +9 -1
  18. package/netlify.toml +1 -0
  19. package/node_modules/undici/docs/docs/api/Dispatcher.md +51 -0
  20. package/node_modules/undici/index.js +2 -1
  21. package/node_modules/undici/lib/api/api-request.js +1 -1
  22. package/node_modules/undici/lib/core/connect.js +5 -0
  23. package/node_modules/undici/lib/dispatcher/client-h2.js +20 -6
  24. package/node_modules/undici/lib/handler/retry-handler.js +3 -3
  25. package/node_modules/undici/lib/interceptor/dns.js +375 -0
  26. package/node_modules/undici/lib/web/cache/cache.js +1 -0
  27. package/node_modules/undici/lib/web/cache/cachestorage.js +2 -0
  28. package/node_modules/undici/lib/web/eventsource/eventsource.js +2 -0
  29. package/node_modules/undici/lib/web/fetch/body.js +9 -1
  30. package/node_modules/undici/lib/web/fetch/formdata.js +2 -0
  31. package/node_modules/undici/lib/web/fetch/headers.js +2 -0
  32. package/node_modules/undici/lib/web/fetch/index.js +1 -1
  33. package/node_modules/undici/lib/web/fetch/request.js +1 -0
  34. package/node_modules/undici/lib/web/fetch/response.js +1 -0
  35. package/node_modules/undici/lib/web/fetch/webidl.js +2 -0
  36. package/node_modules/undici/lib/web/websocket/events.js +4 -0
  37. package/node_modules/undici/lib/web/websocket/websocket.js +2 -0
  38. package/node_modules/undici/package.json +1 -1
  39. package/node_modules/undici/types/interceptors.d.ts +14 -0
  40. package/node_modules/undici/types/retry-handler.d.ts +1 -1
  41. package/node_modules/undici/types/webidl.d.ts +6 -0
  42. package/package.json +6 -6
  43. package/release-aliases/3-EDGE +1 -1
  44. package/release-aliases/3-STABLE +1 -1
  45. package/scripts/install-docker-desktop.sh +1 -1
  46. package/scripts/install-docker-engine.sh +1 -1
  47. package/scripts/lando-entrypoint.sh +1 -1
  48. package/utils/build-tooling-task.js +2 -1
  49. package/utils/get-compose-x.js +1 -1
  50. package/utils/get-config-defaults.js +5 -5
  51. package/utils/get-tasks.js +4 -2
  52. package/utils/load-compose-files.js +2 -2
  53. package/utils/to-lando-container.js +16 -2
  54. package/checksums.txt +0 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  ## {{ UNRELEASED_VERSION }} - [{{ UNRELEASED_DATE }}]({{ UNRELEASED_LINK }})
2
2
 
3
- ## v3.23.22 - [January 6, 2025](https://github.com/florianPat/lando-core/releases/tag/v3.23.22)
3
+ ## v3.23.27-2florianPat.0 - [February 19, 2025](https://github.com/florianPat/lando-core/releases/tag/v3.23.27-2florianPat.0)
4
+
5
+ ## v3.23.26 - [January 24, 2025](https://github.com/lando/core/releases/tag/v3.23.26)
6
+
7
+ * Fixed bug where an app’s services were inadvertently reaped if the app’s path included a comma [#322](https://github.com/lando/core/issues/322)
8
+
9
+ ## v3.23.25 - [January 18, 2025](https://github.com/lando/core/releases/tag/v3.23.25)
10
+
11
+ * Fixed bug causing `--accept-license` flag to not work when installing Docker Desktop on macOS
12
+ * Updated default Docker Desktop version to `4.37.1|2`
13
+ * Updated default Docker Engine version to `27.5.0`
14
+ * Updated default Docker Compose version to `2.31.0`
15
+ * Updated recommended Docker Desktop range to `>=4.37.0`
16
+ * Updated tested Docker Desktop range to `<=4.37`
17
+ * Updated tested Docker Compose range to `<=2.32`
18
+
19
+ ## v3.23.24 - [January 14, 2025](https://github.com/lando/core/releases/tag/v3.23.24)
20
+
21
+ * Fixed bug causing service script moving to fail when receiving non-stringy inputs
22
+
23
+ ## v3.23.23 - [January 14, 2025](https://github.com/lando/core/releases/tag/v3.23.23)
24
+
25
+ * Fixed bug causing service script loading collisions
4
26
 
5
27
  ## v3.23.22 - [December 17, 2024](https://github.com/lando/core/releases/tag/v3.23.22)
6
28
 
package/app.js CHANGED
@@ -113,6 +113,9 @@ module.exports = async (app, lando) => {
113
113
  // Add tooling if applicable
114
114
  app.events.on('post-init', async () => await require('./hooks/app-add-tooling')(app, lando));
115
115
 
116
+ // Add _init tooling for bootstrap reference
117
+ app.events.on('pre-bootstrap', async () => await require('./hooks/app-add-init-tooling')(app, lando));
118
+
116
119
  // Collect info so we can inject LANDO_INFO
117
120
  // @NOTE: this is not currently the full lando info because a lot of it requires the app to be on
118
121
  app.events.on('post-init', 10, async () => await require('./hooks/app-set-lando-info')(app, lando));
@@ -240,6 +243,8 @@ module.exports = async (app, lando) => {
240
243
  BITNAMI_DEBUG: 'true',
241
244
  },
242
245
  labels: {
246
+ 'io.lando.landofiles': app.configFiles.map(file => path.basename(file)).join(','),
247
+ 'io.lando.root': app.root,
243
248
  'io.lando.src': app.configFiles.join(','),
244
249
  'io.lando.http-ports': '80,443',
245
250
  },
@@ -41,6 +41,7 @@ module.exports = {
41
41
  refreshCerts = false,
42
42
  remoteFiles = {},
43
43
  scripts = [],
44
+ scriptsDir = false,
44
45
  sport = '443',
45
46
  ssl = false,
46
47
  sslExpose = true,
@@ -68,16 +69,25 @@ module.exports = {
68
69
  console.error(color.yellow(`${type} version ${version} is a legacy version! We recommend upgrading.`));
69
70
  }
70
71
 
72
+ // normalize scripts dir if needed
73
+ if (typeof scriptsDir === 'string' && !path.isAbsolute(scriptsDir)) scriptsDir = path.resolve(root, scriptsDir);
74
+
75
+ // Get some basic locations
76
+ const globalScriptsDir = path.join(userConfRoot, 'scripts');
77
+ const serviceScriptsDir = path.join(userConfRoot, 'helpers', project, type, name);
78
+ const entrypointScript = path.join(globalScriptsDir, 'lando-entrypoint.sh');
79
+ const addCertsScript = path.join(globalScriptsDir, 'add-cert.sh');
80
+ const refreshCertsScript = path.join(globalScriptsDir, 'refresh-certs.sh');
81
+
71
82
  // Move our config into the userconfroot if we have some
72
83
  // NOTE: we need to do this because on macOS and Windows not all host files
73
84
  // are shared into the docker vm
74
85
  if (fs.existsSync(confSrc)) require('../utils/move-config')(confSrc, confDest);
75
86
 
76
- // Get some basic locations
77
- const scriptsDir = path.join(userConfRoot, 'scripts');
78
- const entrypointScript = path.join(scriptsDir, 'lando-entrypoint.sh');
79
- const addCertsScript = path.join(scriptsDir, 'add-cert.sh');
80
- const refreshCertsScript = path.join(scriptsDir, 'refresh-certs.sh');
87
+ // ditto for service helpers
88
+ if (!require('../utils/is-disabled')(scriptsDir) && typeof scriptsDir === 'string' && fs.existsSync(scriptsDir)) {
89
+ require('../utils/move-config')(scriptsDir, serviceScriptsDir);
90
+ }
81
91
 
82
92
  // Handle Environment
83
93
  const environment = {
@@ -100,10 +110,13 @@ module.exports = {
100
110
  // Handle volumes
101
111
  const volumes = [
102
112
  `${userConfRoot}:/lando:cached`,
103
- `${scriptsDir}:/helpers`,
113
+ `${globalScriptsDir}:/helpers`,
104
114
  `${entrypointScript}:/lando-entrypoint.sh`,
105
115
  ];
106
116
 
117
+ // add in service helpers if we have them
118
+ if (fs.existsSync(serviceScriptsDir)) volumes.push(`${serviceScriptsDir}:/etc/lando/service/helpers`);
119
+
107
120
  // Handle ssl
108
121
  if (ssl) {
109
122
  // also expose the sport
@@ -443,7 +443,10 @@ module.exports = {
443
443
  const labels = merge({}, app.labels, {
444
444
  'dev.lando.container': 'TRUE',
445
445
  'dev.lando.id': lando.config.id,
446
+ 'dev.lando.landofiles': app.configFiles.map(file => path.basename(file)).join(','),
447
+ 'dev.lando.root': app.root,
446
448
  'dev.lando.src': app.root,
449
+ 'io.lando.http-ports': '80,443',
447
450
  }, config.labels);
448
451
 
449
452
  // add it all 2getha
package/config.yml CHANGED
@@ -13,21 +13,21 @@ dockerSupportedVersions:
13
13
  compose:
14
14
  satisfies: "1.x.x || 2.x.x"
15
15
  recommendUpdate: "<=2.24.6"
16
- tested: "<=2.30.99"
16
+ tested: "<=2.32.99"
17
17
  link:
18
18
  linux: https://docs.docker.com/compose/install/#install-compose-on-linux-systems
19
19
  darwin: https://docs.docker.com/desktop/install/mac-install/
20
20
  win32: https://docs.docker.com/desktop/install/windows-install/
21
21
  desktop:
22
22
  satisfies: ">=4.0.0 <5"
23
- tested: "<=4.36.99"
24
- recommendUpdate: "<=4.34.0"
23
+ tested: "<=4.37.99"
24
+ recommendUpdate: "<=4.36"
25
25
  link:
26
26
  darwin: https://docs.docker.com/desktop/install/mac-install/
27
27
  win32: https://docs.docker.com/desktop/install/windows-install/
28
28
  wsl: https://docs.docker.com/desktop/install/windows-install/
29
29
  engine:
30
30
  satisfies: ">=18 <28"
31
- tested: "<=27.3.1"
31
+ tested: "<=27.5.99"
32
32
  link:
33
33
  linux: https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+
5
+ module.exports = async (app, lando) => {
6
+ if (!_.isEmpty(_.get(app, 'config.tooling', {}))) {
7
+ app.log.verbose('additional tooling detected');
8
+
9
+ // Add the _init tasks for the bootstrap event!
10
+ // TODO(flo): They are duplicated through "app-add-tooling" but I do not care for now!
11
+ _.forEach(require('../utils/get-tooling-tasks')(app.config.tooling, app), task => {
12
+ if (task.service !== '_init') {
13
+ return;
14
+ }
15
+
16
+ app.log.debug('adding app cli task %s', task.name);
17
+ const injectable = _.has(app, 'engine') ? app : lando;
18
+ app.tasks.push(require('../utils/build-tooling-task')(task, injectable));
19
+ });
20
+ }
21
+ };
@@ -7,6 +7,28 @@ const formatters = require('../lib/formatters');
7
7
 
8
8
  module.exports = async (app, lando, cmds, data, event) => {
9
9
  const eventCommands = require('./../utils/parse-events-config')(cmds, app, data, lando);
10
+ // add perm sweeping to all v3 services
11
+ if (!_.isEmpty(eventCommands)) {
12
+ const permsweepers = _(eventCommands)
13
+ .filter(command => command.api === 3)
14
+ .map(command => ({id: command.id, services: _.get(command, 'opts.services', [])}))
15
+ .uniqBy('id')
16
+ .value();
17
+ lando.log.debug('added preemptive perm sweeping to evented v3 services %j', permsweepers.map(s => s.id));
18
+ _.forEach(permsweepers, ({id, services}) => {
19
+ eventCommands.unshift({
20
+ id,
21
+ cmd: '/helpers/user-perms.sh --silent',
22
+ compose: app.compose,
23
+ project: app.project,
24
+ opts: {
25
+ mode: 'attach',
26
+ user: 'root',
27
+ services,
28
+ },
29
+ });
30
+ });
31
+ }
10
32
  const injectable = _.has(app, 'engine') ? app : lando;
11
33
 
12
34
  const splitEventCommands = [];
@@ -10,6 +10,9 @@ const semver = require('semver');
10
10
  const {color} = require('listr2');
11
11
 
12
12
  const buildIds = {
13
+ '4.37.2': '179585',
14
+ '4.37.1': '178610',
15
+ '4.37.0': '178034',
13
16
  '4.36.0': '175267',
14
17
  '4.35.1': '173168',
15
18
  '4.35.0': '172550',
@@ -10,6 +10,8 @@ const {color} = require('listr2');
10
10
  const {nanoid} = require('nanoid');
11
11
 
12
12
  const buildIds = {
13
+ '4.37.1': '178610',
14
+ '4.37.0': '178034',
13
15
  '4.36.0': '175267',
14
16
  '4.35.1': '173168',
15
17
  '4.35.0': '172550',
@@ -12,6 +12,8 @@ const {color} = require('listr2');
12
12
  const {nanoid} = require('nanoid');
13
13
 
14
14
  const buildIds = {
15
+ '4.37.1': '178610',
16
+ '4.37.0': '178034',
15
17
  '4.36.0': '175267',
16
18
  '4.35.1': '173168',
17
19
  '4.35.0': '172550',
@@ -7,7 +7,7 @@ const path = require('path');
7
7
  /*
8
8
  * Helper to get docker compose v2 download url
9
9
  */
10
- const getComposeDownloadUrl = (version = '2.30.3') => {
10
+ const getComposeDownloadUrl = (version = '2.31.0') => {
11
11
  const mv = version.split('.')[0] > 1 ? '2' : '1';
12
12
  const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';
13
13
  const toggle = `${process.platform}-${mv}`;
@@ -31,7 +31,7 @@ const getComposeDownloadUrl = (version = '2.30.3') => {
31
31
  /*
32
32
  * Helper to get docker compose v2 download destination
33
33
  */
34
- const getComposeDownloadDest = (base, version = '2.30.3') => {
34
+ const getComposeDownloadDest = (base, version = '2.31.0') => {
35
35
  switch (process.platform) {
36
36
  case 'linux':
37
37
  case 'darwin':
package/lib/app.js CHANGED
@@ -11,8 +11,8 @@ const fs = require('node:fs');
11
11
  /*
12
12
  * Helper to init and then report
13
13
  */
14
- const initAndReport = (app, method = 'start') => {
15
- return app.init().then(() => {
14
+ const initAndReport = (app, method, shouldBootstrap = false) => {
15
+ return app.init({shouldBootstrap}).then(() => {
16
16
  app.metrics.report(method, utils.metricsParse(app));
17
17
  return Promise.resolve(true);
18
18
  });
@@ -257,6 +257,8 @@ module.exports = class App {
257
257
  .then(() => this.log.info('destroyed app.'));
258
258
  };
259
259
 
260
+ static isBootstrapCommand = undefined;
261
+
260
262
  /**
261
263
  * Initializes the app
262
264
  *
@@ -270,21 +272,37 @@ module.exports = class App {
270
272
  * @fires ready
271
273
  * @return {Promise} A Promise.
272
274
  */
273
- init({noEngine = false} = {}) {
275
+ init({noEngine = false, shouldBootstrap = false} = {}) {
274
276
  // We should only need to initialize once, if we have just go right to app ready
275
277
  if (this.initialized) return this.events.emit('ready', this);
278
+ if (undefined === App.isBootstrapCommand) {
279
+ App.isBootstrapCommand = !fs.existsSync(this._dir);
280
+ }
281
+ const bootstrapping = App.isBootstrapCommand && shouldBootstrap && !noEngine;
282
+ if (bootstrapping) {
283
+ console.log(require('yargonaut').chalk().cyan('Looks like this is the first time to start the app. Lets bootstrap it...'));
284
+ }
285
+
286
+ return loadPlugins(this, this._lando)
287
+ /**
288
+ * Event that only gets triggered if the app never started before (or was destroyed)
289
+ *
290
+ * @since 3.23.25
291
+ * @alias app.events:pre-bootstrap
292
+ * @event pre-bootstrap
293
+ * @property {App} app The app instance.
294
+ */
295
+ .then(() => bootstrapping ? this.events.emit('pre-bootstrap', this) : undefined)
276
296
  // Get compose data if we have any, otherwise set to []
277
- return require('../utils/load-compose-files')(
297
+ .then(() => noEngine === true ? [] : require('../utils/load-compose-files')(
278
298
  _.get(this, 'config.compose', []),
279
299
  this.root,
280
300
  this._dir,
281
301
  (composeFiles, outputFilePath) =>
282
302
  this.engine.getComposeConfig({compose: composeFiles, project: this.project, outputFilePath}),
283
- )
303
+ ))
284
304
  .then(composeFileData => {
285
- if (undefined !== composeFileData) {
286
- this.composeData = [new this.ComposeService('compose', {}, composeFileData)];
287
- }
305
+ this.composeData = [new this.ComposeService('compose', {}, ...composeFileData)];
288
306
  // Validate and set env files
289
307
  this.envFiles = require('../utils/normalize-files')(_.get(this, 'config.env_file', []), this.root);
290
308
  // Log some things
@@ -303,8 +321,6 @@ module.exports = class App {
303
321
  * @event pre_init
304
322
  * @property {App} app The app instance.
305
323
  */
306
- .then(() => loadPlugins(this, this._lando))
307
-
308
324
  .then(() => this.events.emit('pre-init', this))
309
325
  // Actually assemble this thing so its ready for that engine
310
326
  .then(() => {
@@ -501,25 +517,15 @@ module.exports = class App {
501
517
  * @alias app.start
502
518
  * @fires pre_start
503
519
  * @fires post_start
520
+ * @fires post_bootstrap
504
521
  * @return {Promise} A Promise.
505
522
  *
506
523
  */
507
524
  start() {
508
525
  // Log
509
526
  this.log.info('starting app...');
510
- const shouldBootstrap = fs.existsSync(this._dir);
511
-
512
- return initAndReport(this)
513
527
 
514
- /**
515
- * Event that only gets triggered if the app never started before (or was destroyed)
516
- *
517
- * @since 3.22.3
518
- * @alias app.events:pre-bootstrap
519
- * @event pre-bootstrap
520
- * @property {App} app The app instance.
521
- */
522
- .then(() => shouldBootstrap ? this.events.emit('pre-bootstrap', this) : undefined)
528
+ return initAndReport(this, 'start', true)
523
529
 
524
530
  /**
525
531
  * Event that runs before an app starts up.
@@ -551,12 +557,12 @@ module.exports = class App {
551
557
  /**
552
558
  * Event that only gets triggered if the app never started before (or was destroyed)
553
559
  *
554
- * @since 3.22.3
560
+ * @since 3.23.25
555
561
  * @alias app.events:post-bootstrap
556
562
  * @event post-bootstrap
557
563
  * @property {App} app The app instance.
558
564
  */
559
- .then(() => shouldBootstrap ? this.events.emit('post-bootstrap', this) : undefined)
565
+ .then(() => App.isBootstrapCommand ? this.events.emit('post-bootstrap', this) : undefined)
560
566
 
561
567
  .then(() => this.log.info('started app.'));
562
568
  };
package/lib/daemon.js CHANGED
@@ -53,7 +53,7 @@ module.exports = class LandoDaemon {
53
53
  log = new Log(),
54
54
  context = 'node',
55
55
  compose = require('../utils/get-compose-x')(),
56
- orchestratorVersion = '2.30.3',
56
+ orchestratorVersion = '2.31.0',
57
57
  userConfRoot = path.join(os.homedir(), '.lando'),
58
58
  ) {
59
59
  this.cache = cache;
package/lib/engine.js CHANGED
@@ -172,7 +172,7 @@ module.exports = class Engine {
172
172
  * return lando.engine.exists(compose);
173
173
  */
174
174
  exists(data) {
175
- return this.engineCmd('exists', data);
175
+ return this.engineCmd('exists', _.merge({}, {separator: this.separator}, data));
176
176
  };
177
177
 
178
178
  /*
package/lib/formatters.js CHANGED
@@ -135,7 +135,7 @@ exports.handleInteractive = (inquiry, argv, command, lando) => lando.Promise.try
135
135
  // NOTE: We need to clone deep here otherwise any apps with interactive options get 2x all their events
136
136
  // NOTE: Not exactly clear on why app here gets conflated with the app returned from lando.getApp
137
137
  const app = _.cloneDeep(lando.getApp(argv._app.root));
138
- return app.init().then(() => {
138
+ return app.init({noEngine: true}).then(() => {
139
139
  inquiry = exports.getInteractive(_.find(app.tasks.concat(lando.tasks), {command: command}).options, argv);
140
140
  return inquirer.prompt(_.sortBy(inquiry, 'weight'));
141
141
  });
package/lib/router.js CHANGED
@@ -55,7 +55,7 @@ exports.destroy = (data, compose, docker) => retryEach(data, datum => {
55
55
  exports.exists = (data, compose, docker, ids = []) => {
56
56
  if (data.compose) return compose('getId', data).then(id => !_.isEmpty(id));
57
57
  else {
58
- return docker.list()
58
+ return docker.list({}, data.separator)
59
59
  .each(container => {
60
60
  ids.push(container.id);
61
61
  ids.push(container.name);
package/lib/updates.js CHANGED
@@ -127,7 +127,15 @@ module.exports = class UpdateManager {
127
127
  const {data, status, url} = await octokit.rest.repos.listReleases({owner: 'lando', repo: 'core'});
128
128
  this.debug('retrieved cli information from %o [%o]', url, status);
129
129
 
130
- const newestCoreVersion = semver.clean((await this.plugins.find(plugin => plugin.core)?.check4Update())?.update?.version ?? '');
130
+ const corePlugin = await this.plugins.find(plugin => plugin.core);
131
+ if (undefined === corePlugin) {
132
+ throw new Error('We should find a core!');
133
+ }
134
+ let newestCoreVersion = corePlugin.check4Update()?.update?.version ?? corePlugin.version;
135
+ if (undefined === newestCoreVersion) {
136
+ throw new Error('Could not obtain the next lando core version!');
137
+ }
138
+ newestCoreVersion = semver.clean(newestCoreVersion);
131
139
 
132
140
  const versions = data
133
141
  .map(release => ({...release, version: semver.clean(release.tag_name)}))
package/netlify.toml CHANGED
@@ -20,6 +20,7 @@
20
20
  "https://docs.google.com/document",
21
21
  "https://docs.google.com/forms",
22
22
  "https://github.com",
23
+ "https://www.drupal.org/community/events",
23
24
  "/v/"
24
25
  ]
25
26
  skipPatterns = [
@@ -986,6 +986,57 @@ client.dispatch(
986
986
  );
987
987
  ```
988
988
 
989
+ ##### `dns`
990
+
991
+ The `dns` interceptor enables you to cache DNS lookups for a given duration, per origin.
992
+
993
+ >It is well suited for scenarios where you want to cache DNS lookups to avoid the overhead of resolving the same domain multiple times
994
+
995
+ **Options**
996
+ - `maxTTL` - The maximum time-to-live (in milliseconds) of the DNS cache. It should be a positive integer. Default: `10000`.
997
+ - Set `0` to disable TTL.
998
+ - `maxItems` - The maximum number of items to cache. It should be a positive integer. Default: `Infinity`.
999
+ - `dualStack` - Whether to resolve both IPv4 and IPv6 addresses. Default: `true`.
1000
+ - It will also attempt a happy-eyeballs-like approach to connect to the available addresses in case of a connection failure.
1001
+ - `affinity` - Whether to use IPv4 or IPv6 addresses. Default: `4`.
1002
+ - It can be either `'4` or `6`.
1003
+ - It will only take effect if `dualStack` is `false`.
1004
+ - `lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void` - Custom lookup function. Default: `dns.lookup`.
1005
+ - For more info see [dns.lookup](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback).
1006
+ - `pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord` - Custom pick function. Default: `RoundRobin`.
1007
+ - The function should return a single record from the records array.
1008
+ - By default a simplified version of Round Robin is used.
1009
+ - The `records` property can be mutated to store the state of the balancing algorithm.
1010
+
1011
+ > The `Dispatcher#options` also gets extended with the options `dns.affinity`, `dns.dualStack`, `dns.lookup` and `dns.pick` which can be used to configure the interceptor at a request-per-request basis.
1012
+
1013
+
1014
+ **DNSInterceptorRecord**
1015
+ It represents a DNS record.
1016
+ - `family` - (`number`) The IP family of the address. It can be either `4` or `6`.
1017
+ - `address` - (`string`) The IP address.
1018
+
1019
+ **DNSInterceptorOriginRecords**
1020
+ It represents a map of DNS IP addresses records for a single origin.
1021
+ - `4.ips` - (`DNSInterceptorRecord[] | null`) The IPv4 addresses.
1022
+ - `6.ips` - (`DNSInterceptorRecord[] | null`) The IPv6 addresses.
1023
+
1024
+ **Example - Basic DNS Interceptor**
1025
+
1026
+ ```js
1027
+ const { Client, interceptors } = require("undici");
1028
+ const { dns } = interceptors;
1029
+
1030
+ const client = new Agent().compose([
1031
+ dns({ ...opts })
1032
+ ])
1033
+
1034
+ const response = await client.request({
1035
+ origin: `http://localhost:3030`,
1036
+ ...requestOpts
1037
+ })
1038
+ ```
1039
+
989
1040
  ##### `Response Error Interceptor`
990
1041
 
991
1042
  **Introduction**
@@ -41,7 +41,8 @@ module.exports.createRedirectInterceptor = createRedirectInterceptor
41
41
  module.exports.interceptors = {
42
42
  redirect: require('./lib/interceptor/redirect'),
43
43
  retry: require('./lib/interceptor/retry'),
44
- dump: require('./lib/interceptor/dump')
44
+ dump: require('./lib/interceptor/dump'),
45
+ dns: require('./lib/interceptor/dns')
45
46
  }
46
47
 
47
48
  module.exports.buildConnector = buildConnector
@@ -73,7 +73,7 @@ class RequestHandler extends AsyncResource {
73
73
  this.removeAbortListener = util.addAbortListener(this.signal, () => {
74
74
  this.reason = this.signal.reason ?? new RequestAbortedError()
75
75
  if (this.res) {
76
- util.destroy(this.res, this.reason)
76
+ util.destroy(this.res.on('error', util.nop), this.reason)
77
77
  } else if (this.abort) {
78
78
  this.abort(this.reason)
79
79
  }
@@ -220,6 +220,11 @@ const setupConnectTimeout = process.platform === 'win32'
220
220
  * @param {number} opts.port
221
221
  */
222
222
  function onConnectTimeout (socket, opts) {
223
+ // The socket could be already garbage collected
224
+ if (socket == null) {
225
+ return
226
+ }
227
+
223
228
  let message = 'Connect Timeout Error'
224
229
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
225
230
  message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
@@ -31,6 +31,8 @@ const {
31
31
 
32
32
  const kOpenStreams = Symbol('open streams')
33
33
 
34
+ let extractBody
35
+
34
36
  // Experimental
35
37
  let h2ExperimentalWarned = false
36
38
 
@@ -240,11 +242,12 @@ function onHTTP2GoAway (code) {
240
242
  util.destroy(this[kSocket], err)
241
243
 
242
244
  // Fail head of pipeline.
243
- const request = client[kQueue][client[kRunningIdx]]
244
- client[kQueue][client[kRunningIdx]++] = null
245
- util.errorRequest(client, request, err)
246
-
247
- client[kPendingIdx] = client[kRunningIdx]
245
+ if (client[kRunningIdx] < client[kQueue].length) {
246
+ const request = client[kQueue][client[kRunningIdx]]
247
+ client[kQueue][client[kRunningIdx]++] = null
248
+ util.errorRequest(client, request, err)
249
+ client[kPendingIdx] = client[kRunningIdx]
250
+ }
248
251
 
249
252
  assert(client[kRunning] === 0)
250
253
 
@@ -260,7 +263,8 @@ function shouldSendContentLength (method) {
260
263
 
261
264
  function writeH2 (client, request) {
262
265
  const session = client[kHTTP2Session]
263
- const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
266
+ const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
267
+ let { body } = request
264
268
 
265
269
  if (upgrade) {
266
270
  util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
@@ -381,6 +385,16 @@ function writeH2 (client, request) {
381
385
 
382
386
  let contentLength = util.bodyLength(body)
383
387
 
388
+ if (util.isFormDataLike(body)) {
389
+ extractBody ??= require('../web/fetch/body.js').extractBody
390
+
391
+ const [bodyStream, contentType] = extractBody(body)
392
+ headers['content-type'] = contentType
393
+
394
+ body = bodyStream.stream
395
+ contentLength = bodyStream.length
396
+ }
397
+
384
398
  if (contentLength == null) {
385
399
  contentLength = request.contentLength
386
400
  }
@@ -229,7 +229,7 @@ class RetryHandler {
229
229
  return false
230
230
  }
231
231
 
232
- const { start, size, end = size } = contentRange
232
+ const { start, size, end = size - 1 } = contentRange
233
233
 
234
234
  assert(this.start === start, 'content-range mismatch')
235
235
  assert(this.end == null || this.end === end, 'content-range mismatch')
@@ -252,7 +252,7 @@ class RetryHandler {
252
252
  )
253
253
  }
254
254
 
255
- const { start, size, end = size } = range
255
+ const { start, size, end = size - 1 } = range
256
256
  assert(
257
257
  start != null && Number.isFinite(start),
258
258
  'content-range mismatch'
@@ -266,7 +266,7 @@ class RetryHandler {
266
266
  // We make our best to checkpoint the body for further range headers
267
267
  if (this.end == null) {
268
268
  const contentLength = headers['content-length']
269
- this.end = contentLength != null ? Number(contentLength) : null
269
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
270
270
  }
271
271
 
272
272
  assert(Number.isFinite(this.start))