Repository: auth0/node-auth0
Pull request #919 (2023-08-23 13:20:13)

feat: add configuration for using persistent connections
### Changes

Exposing the underlying library's - (rest-facade) options for keepAlive connections

We experienced a lot of ETIMEDOUT errors during our load tests and keeping alive connections mitigated the errors.

Note: more work needed for auth/TokensManager and auth/UsersManager as that is using axios (static way) to do the calls. To be able to use keepAlive there an axios instance need to be created and reused in the requests.

### References

https://github.com/auth0/node-auth0/issues/545

### Testing

Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. 

- [x] This change adds unit test coverage
- [ ] This change adds integration test coverage

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All existing and new tests complete without errors



Patch:
diff --git a/src/auth/DatabaseAuthenticator.js b/src/auth/DatabaseAuthenticator.js
index 3429de2f6..867208169 100644
--- a/src/auth/DatabaseAuthenticator.js
+++ b/src/auth/DatabaseAuthenticator.js
@@ -10,7 +10,8 @@ class DatabaseAuthenticator {
    * @param  {object}              options            Authenticator options.
    * @param  {string}              options.baseUrl    The auth0 account URL.
    * @param  {string}              [options.clientId] Default client ID.
-   * @param   {string}             [options.proxy]    Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
+   * @param  {string}              [options.proxy]    Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
+   * @param  {boolean}             [options.keepAlive] Keep the connection alive
    * @param  {OAuthAuthenticator}  oauth              OAuthAuthenticator instance.
    */
   constructor(options, oauth) {
@@ -31,6 +32,7 @@ class DatabaseAuthenticator {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
       proxy: options.proxy,
+      keepAlive: options.keepAlive,
     };
 
     this.oauth = oauth;
diff --git a/src/auth/OAuthAuthenticator.js b/src/auth/OAuthAuthenticator.js
index 709f561d3..6baae4419 100644
--- a/src/auth/OAuthAuthenticator.js
+++ b/src/auth/OAuthAuthenticator.js
@@ -56,6 +56,7 @@ class OAuthAuthenticator {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
       proxy: options.proxy,
+      keepAlive: options.keepAlive,
     };
 
     this.oauth = new RestClient(`${options.baseUrl}/oauth/:type`, clientOptions);
diff --git a/src/auth/PasswordlessAuthenticator.js b/src/auth/PasswordlessAuthenticator.js
index f20211e35..c3b522cde 100644
--- a/src/auth/PasswordlessAuthenticator.js
+++ b/src/auth/PasswordlessAuthenticator.js
@@ -51,6 +51,7 @@ class PasswordlessAuthenticator {
       errorFormatter: { message: 'message', name: 'error' },
       headers: options.headers,
       proxy: options.proxy,
+      keepAlive: options.keepAlive,
     };
 
     this.oauth = oauth;
diff --git a/src/auth/index.js b/src/auth/index.js
index d2dc7e284..af06ca21d 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -43,6 +43,7 @@ class AuthenticationClient {
    * @param  {boolean}  [options.__bypassIdTokenValidation] Whether the id_token should be validated or not
    * @param   {object}  [options.headers]                 Additional headers that will be added to the outgoing requests.
    * @param   {string}  [options.proxy]                   Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
+   * @param   {boolean}  [options.keepAlive]              Keep the http connections alive.
    */
   constructor(options) {
     if (!options || typeof options !== 'object') {
@@ -69,6 +70,7 @@ class AuthenticationClient {
       supportedAlgorithms: options.supportedAlgorithms,
       __bypassIdTokenValidation: options.__bypassIdTokenValidation,
       proxy: options.proxy,
+      keepAlive: options.keepAlive,
     };
 
     if (options.telemetry !== false) {
diff --git a/src/management/BaseManager.js b/src/management/BaseManager.js
index 248b707fe..afe44a496 100644
--- a/src/management/BaseManager.js
+++ b/src/management/BaseManager.js
@@ -27,6 +27,7 @@ class BaseManager {
       proxy: options.proxy,
       query: { repeatParams: false },
       includeResponseHeaders: options.includeResponseHeaders,
+      keepAlive: options.keepAlive,
     };
 
     const usersAuth0RestClient = new Auth0RestClient(
diff --git a/src/management/index.js b/src/management/index.js
index 9bacac36a..1ba52b223 100644
--- a/src/management/index.js
+++ b/src/management/index.js
@@ -108,7 +108,8 @@ class ManagementClient {
    * @param   {number}  [options.retry.maxRetries=10]               Retry failed requests X times.
    * @param   {object}  [options.headers]                           Additional headers that will be added to the outgoing requests.
    * @param   {string}  [options.proxy]                             Add the `superagent-proxy` dependency and specify a proxy url eg 'https://myproxy.com:1234'
-   * @param   {boolean}  [options.includeResponseHeaders]            Include the response headers in the payload in the format `{ data, headers }`.
+   * @param   {boolean}  [options.includeResponseHeaders]           Include the response headers in the payload in the format `{ data, headers }`.
+   * @param   {boolean}  [options.keepAlive]                        Keep the http connections alive.
    */
   constructor(options) {
     if (!options || typeof options !== 'object') {
@@ -172,6 +173,7 @@ class ManagementClient {
     managerOptions.retry = options.retry;
     managerOptions.includeResponseHeaders = options.includeResponseHeaders;
     managerOptions.proxy = options.proxy;
+    managerOptions.keepAlive = options.keepAlive;
 
     /**
      * Simple abstraction for performing CRUD operations on the


Test patch:
diff --git a/test/auth/authentication-client.tests.js b/test/auth/authentication-client.tests.js
index dd97adcdc..513d1d392 100644
--- a/test/auth/authentication-client.tests.js
+++ b/test/auth/authentication-client.tests.js
@@ -174,6 +174,17 @@ describe('AuthenticationClient', () => {
       expect(client.users.headers).to.contain(customHeaders);
       expect(client.tokens.headers).to.contain(customHeaders);
     });
+    it('should configure instances with keepAlive if the option is passwd', () => {
+      const client = new AuthenticationClient({
+        token: 'token',
+        domain: 'auth0.com',
+        keepAlive: true,
+      });
+
+      expect(client.oauth.oauth.options.keepAlive).to.equal(true);
+      expect(client.database.dbConnections.options.keepAlive).to.equal(true);
+      expect(client.passwordless.passwordless.options.keepAlive).to.equal(true);
+    });
   });
 
   describe(`verifySMSCode`, () => {
diff --git a/test/auth0-rest-client.tests.js b/test/auth0-rest-client.tests.js
index 96aed2b01..9fc795434 100644
--- a/test/auth0-rest-client.tests.js
+++ b/test/auth0-rest-client.tests.js
@@ -313,4 +313,32 @@ describe('Auth0RestClient', () => {
       proxy: 'http://proxy',
     });
   });
+
+  it('should use keepAlive if it is passed in the options', async function () {
+    const spy = sinon.spy();
+    class MockClient extends Client {
+      constructor(...args) {
+        spy(...args);
+        super(...args);
+      }
+    }
+    const RestClient = proxyquire('../src/Auth0RestClient', {
+      'rest-facade': {
+        Client: MockClient,
+      },
+    });
+    nock(API_URL).get('/some-resource').reply(200, { data: 'value' });
+
+    const options = {
+      headers: {},
+      keepAlive: true,
+    };
+
+    const client = new RestClient(`${API_URL}/some-resource`, options, this.providerMock);
+    const data = await client.getAll();
+    expect(data).to.deep.equal({ data: 'value' });
+    sinon.assert.calledWithMatch(spy, 'https://tenant.auth0.com/some-resource', {
+      keepAlive: true,
+    });
+  });
 });
diff --git a/test/management/management-client.tests.js b/test/management/management-client.tests.js
index 33cc89362..8cd2c7ed2 100644
--- a/test/management/management-client.tests.js
+++ b/test/management/management-client.tests.js
@@ -843,6 +843,100 @@ describe('ManagementClient', () => {
           'Auth0-Client'
         );
       });
+      it('should configure instances with keepAlive if the option is passwd', () => {
+        const client = new ManagementClient({
+          token: 'token',
+          domain: 'auth0.com',
+          keepAlive: true,
+        });
+
+        expect(client.clients.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.clientGrants.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.grants.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.users.users.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.multifactor.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.identities.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.userLogs.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.enrollments.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.usersByEmail.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(
+          client.users.recoveryCodeRegenerations.restClient.restClient.options.keepAlive
+        ).to.equal(true);
+        expect(
+          client.users.invalidateRememberBrowsers.restClient.restClient.options.keepAlive
+        ).to.equal(true);
+        expect(client.users.roles.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.users.permissions.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.guardian.enrollments.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.guardian.tickets.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.guardian.factors.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.guardian.factorsTemplates.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+        expect(client.guardian.factorsProviders.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+        expect(
+          client.guardian.factorsPhoneSelectedProvider.restClient.restClient.options.keepAlive
+        ).to.equal(true);
+        expect(
+          client.guardian.factorsPhoneMessageTypes.restClient.restClient.options.keepAlive
+        ).to.equal(true);
+
+        expect(client.customDomains.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+        expect(
+          client.customDomains.vefifyResource.restClient.restClient.options.keepAlive
+        ).to.equal(true);
+
+        expect(client.connections.resource.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.deviceCredentials.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+
+        expect(client.rules.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.blacklistedTokens.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+
+        expect(client.emailProvider.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+
+        expect(client.emailTemplates.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+
+        expect(client.stats.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.tenant.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.jobs.jobs.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.jobs.usersExports.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.tickets.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.logs.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.resourceServers.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+
+        expect(client.emailTemplates.resource.restClient.restClient.options.keepAlive).to.equal(
+          true
+        );
+        expect(client.rulesConfigs.resource.restClient.restClient.options.keepAlive).to.equal(true);
+
+        expect(client.roles.resource.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.roles.permissions.restClient.restClient.options.keepAlive).to.equal(true);
+        expect(client.roles.users.restClient.restClient.options.keepAlive).to.equal(true);
+      });
     });
 
     before(function () {