Repository: cloudant/nodejs-cloudant (js)

Issue:
Unhandled promise rejection with `Error: Callback was already called.`
## Bug Description

Customer reported an unhandled promise rejection:
```
Error: Callback was already called.
    at /opt/app-root/src/node_modules/async/dist/async.js:985:32
    at updateState (/opt/app-root/src/node_modules/@cloudant/cloudant/lib/clientutils.js:53:3)
    at /opt/app-root/src/node_modules/@cloudant/cloudant/lib/clientutils.js:142:11
    at /opt/app-root/src/node_modules/@cloudant/cloudant/plugins/iamauth.js:88:7
```

### 1. Steps to reproduce and the simplest code sample possible to demonstrate the issue
<!--
Outline the steps you take to make the problem happen.
Provide the simplest code sample you can, in context, that reproduces the issue.
-->

The problem was reported from a production app without debug enabled. Only the `iamauth` plugin is active.

### 2. What you expected to happen

One callback at the completion of the plugin async eachseries.

### 3. What actually happened

Two callbacks.
Based on the stack my best guess is that it is possible for an error during a success callback to cause an error that will in turn be caught by the promise chain and trigger a second callback.

## Environment details

- Version(s) that are affected by this issue.
    > 4.3.0
- Node.js version
    > 14.16.0



Pull request:
454 double callback
<!--
Thanks for your hard work, please ensure all items are complete before opening.
-->
## Checklist

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](../blob/master/DCO1.1.txt)
- [x] Added tests for code changes _or_ test/build only changes
- [x] Updated the change log file (`CHANGES.md`|`CHANGELOG.md`) _or_ test/build only changes
- [x] Completed the PR template below:

## Description

Correct opportunities for double callbacks

Fixes #454 

## Approach

When writing tests I noticed that max-age was not correctly parsed from a single set cookie header (only from an array) - so there is one fix to resolve that issue.

Double callback (case 1):
In auth plugins for case where an error is encountered during state update.
The `cookieauth` and `iamauth` plugins are modified to `callback(state)` in a `finally` at the end of a promise chain instead of from two places in the `onRequest` function.

Double callback (case 2):
Discovered when updating nock to 13.x (see Test section below). In the case a request is aborted for a retry a double request `end` event might be received because (to [more precisely mirror current Node](https://github.com/nock/nock/blob/main/migration_guides/migrating_to_13.md#upgrading-from-nock-12-to-nock-13) behaviours the `abort` event moved into the next tick. To ensure our plugins behave correctly with the aborts the callbacks in `lib/clientutils.js` `processState` are deferred to the next tick.

## Schema & API Changes

- Remove EOL Node.js versions from test matrix and supported engines

## Security and Privacy

- Fix package-lock.json for a npm audit warning

## Testing

* Moved `test/mocha.opts` -> `.mocharc.json` to avoid warning for using deprecated format.
* Existing tests provide good coverage that functionality is unaffected.
    * For the case 1 double callback I could monkey-patch the state update to throw an error to demonstrate the problem (and did so during manual testing), but I don't think it would provide any value in automated test.
* As I was initially investigating I wrote a couple of extra tests for some IAM auth error cases.
* Upgraded nock to 13.x (to resolve deprecation warnings from Node versions >=10)
     * Caused `CloudantClient/#db using listeners/with ComplexPlugin2` to fail with the `end` event being triggered twice, causing two done callbacks. This is similar to the reported issue so another fix (described above) was introduced to resolve the failure.
     * This test covers the case 2 double callback

## Monitoring and Logging

- "No change"

Patch:
diff --git a/.mocharc.json b/.mocharc.json
new file mode 100644
index 0000000..fadf841
--- /dev/null
+++ b/.mocharc.json
@@ -0,0 +1,5 @@
+{
+"exclude": "test/typescript/*.js",
+"recursive": true,
+"timeout": 60000
+}
diff --git a/.travis.yml b/.travis.yml
index 8e8f260..a0dae24 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,8 +6,8 @@ branches:
     - rewrite
 node_js:
   - "node"
-  - "10"
-  - "8"
+  - "14"
+  - "12"
 os:
   - linux
 before_install:
diff --git a/CHANGES.md b/CHANGES.md
index 4a37227..eb0f14e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,8 @@
+# Unreleased
+- [FIXED] Parsing of max-age from Set-Cookie headers.
+- [FIXED] Double callback if plugin errors when updating state.
+- [NOTE] Updated engines to remove EOL Node.js versions (minimum is now Node.js 12).
+
 # 4.3.1 (2021-03-17)
 - [NEW] Add migration guide to the newly supported cloudant-node-sdk
   (package: @ibm-cloud/cloudant).
diff --git a/Jenkinsfile b/Jenkinsfile
index e65b21c..53c1087 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -76,13 +76,13 @@ stage('Build') {
 
 stage('QA') {
   parallel([
-    Node8x : {
-      //8.x LTS
-      setupNodeAndTest('lts/carbon')
+    Node12x : {
+      //12.x LTS
+      setupNodeAndTest('lts/erbium')
     },
-    Node10x : {
-      //10.x LTS
-      setupNodeAndTest('lts/dubnium')
+    Node14x : {
+      //14.x LTS
+      setupNodeAndTest('lts/fermium')
     },
     Node : {
       // Current
diff --git a/lib/clientutils.js b/lib/clientutils.js
index 6104390..87aa867 100644
--- a/lib/clientutils.js
+++ b/lib/clientutils.js
@@ -1,4 +1,4 @@
-// Copyright © 2017, 2018 IBM Corp. All rights reserved.
+// Copyright © 2017, 2021 IBM Corp. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -67,7 +67,7 @@ var processState = function(r, callback) {
   if (r.abort) {
     // [1] => Client has called for the request to be aborted.
     abort();
-    callback(new Error('Client issued abort')); // no retry
+    setImmediate(callback, new Error('Client issued abort')); // no retry
     return;
   }
 
@@ -77,7 +77,7 @@ var processState = function(r, callback) {
     sendResponseToClient(r.state.abortWithResponse, r.clientStream, r.clientCallback);
     var err = new Error('Plugin issued abort');
     err.skipClientCallback = true;
-    callback(err); // no retry
+    setImmediate(callback, err); // no retry
     return;
   }
 
@@ -85,7 +85,7 @@ var processState = function(r, callback) {
     // [3] => One or more plugins have called for the request to be retried.
     abort();
     debug('Plugin issued a retry.');
-    callback(); // retry
+    setImmediate(callback); // retry
     return;
   }
 
@@ -104,7 +104,7 @@ var processState = function(r, callback) {
 
   if (!r.state.sending) {
     // [4] => Request has not yet been sent. Still processing 'onRequest' hooks.
-    callback(); // continue
+    setImmediate(callback); // continue
     return;
   }
 
@@ -120,7 +120,7 @@ var processState = function(r, callback) {
   }
 
   // [5] => Return response to awaiting client.
-  callback(new Error('No retry requested')); // no retry
+  setImmediate(callback, new Error('No retry requested')); // no retry
 };
 
 // execute a specified hook for all plugins
diff --git a/lib/tokens/TokenManager.js b/lib/tokens/TokenManager.js
index c21500f..c3aa364 100644
--- a/lib/tokens/TokenManager.js
+++ b/lib/tokens/TokenManager.js
@@ -1,4 +1,4 @@
-// Copyright © 2019 IBM Corp. All rights reserved.
+// Copyright © 2019, 2021 IBM Corp. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -32,7 +32,9 @@ class TokenManager {
   _autoRenew(defaultMaxAgeSecs) {
     debug('Auto renewing token now...');
     this._renew().then((response) => {
-      let maxAgeSecs = cookie.parse(response.headers['set-cookie'][0])['Max-Age'] || defaultMaxAgeSecs;
+      let setCookieHeader = response.headers['set-cookie'];
+      let headerValue = Array.isArray(setCookieHeader) ? setCookieHeader[0] : setCookieHeader;
+      let maxAgeSecs = cookie.parse(headerValue)['Max-Age'] || defaultMaxAgeSecs;
       let delayMSecs = maxAgeSecs / 2 * 1000;
       debug(`Renewing token in ${delayMSecs} milliseconds.`);
       setTimeout(this._autoRenew.bind(this, defaultMaxAgeSecs), delayMSecs).unref();
diff --git a/package-lock.json b/package-lock.json
index 1baab80..6d9ddef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -148,12 +148,6 @@
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
     },
-    "assertion-error": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
-      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
-      "dev": true
-    },
     "async": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz",
@@ -298,20 +292,6 @@
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
     },
-    "chai": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
-      "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
-      "dev": true,
-      "requires": {
-        "assertion-error": "^1.0.1",
-        "check-error": "^1.0.1",
-        "deep-eql": "^3.0.0",
-        "get-func-name": "^2.0.0",
-        "pathval": "^1.0.0",
-        "type-detect": "^4.0.0"
-      }
-    },
     "chalk": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@@ -331,12 +311,6 @@
       "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
       "dev": true
     },
-    "check-error": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
-      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
-      "dev": true
-    },
     "chokidar": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
@@ -533,21 +507,6 @@
       "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
       "dev": true
     },
-    "deep-eql": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
-      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
-      "dev": true,
-      "requires": {
-        "type-detect": "^4.0.0"
-      }
-    },
-    "deep-equal": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
-      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
-      "dev": true
-    },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@@ -1106,12 +1065,6 @@
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
       "dev": true
     },
-    "get-func-name": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
-      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
-      "dev": true
-    },
     "getpass": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -1135,9 +1088,9 @@
       }
     },
     "glob-parent": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
       "dev": true,
       "requires": {
         "is-glob": "^4.0.1"
@@ -1561,6 +1514,12 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
     },
+    "lodash.set": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+      "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
+      "dev": true
+    },
     "log-symbols": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
@@ -1834,20 +1793,32 @@
       "dev": true
     },
     "nock": {
-      "version": "9.6.1",
-      "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz",
-      "integrity": "sha512-EDgl/WgNQ0C1BZZlASOQkQdE6tAWXJi8QQlugqzN64JJkvZ7ILijZuG24r4vCC7yOfnm6HKpne5AGExLGCeBWg==",
+      "version": "13.1.0",
+      "resolved": "https://registry.npmjs.org/nock/-/nock-13.1.0.tgz",
+      "integrity": "sha512-3N3DUY8XYrxxzWazQ+nSBpiaJ3q6gcpNh4gXovC/QBxrsvNp4tq+wsLHF6mJ3nrn3lPLn7KCJqKxy/9aD+0fdw==",
       "dev": true,
       "requires": {
-        "chai": "^4.1.2",
-        "debug": "^3.1.0",
-        "deep-equal": "^1.0.0",
+        "debug": "^4.1.0",
         "json-stringify-safe": "^5.0.1",
-        "lodash": "^4.17.5",
-        "mkdirp": "^0.5.0",
-        "propagate": "^1.0.0",
-        "qs": "^6.5.1",
-        "semver": "^5.5.0"
+        "lodash.set": "^4.3.2",
+        "propagate": "^2.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+          "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
       }
     },
     "node-environment-flags": {
@@ -2099,12 +2070,6 @@
         "pify": "^2.0.0"
       }
     },
-    "pathval": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
-      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
-      "dev": true
-    },
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -2180,9 +2145,9 @@
       }
     },
     "propagate": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
-      "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
+      "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
       "dev": true
     },
     "pseudomap": {
@@ -3033,12 +2998,6 @@
         "prelude-ls": "~1.1.2"
       }
     },
-    "type-detect": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
-      "dev": true
-    },
     "typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
diff --git a/package.json b/package.json
index 6e2d3fd..89727c7 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "eslint-plugin-standard": "^3.0.1",
     "md5-file": "^3.2.3",
     "mocha": "^7.1.2",
-    "nock": "^9.2.5",
+    "nock": "^13",
     "should": "6.0.3",
     "typescript": "^2.8.3",
     "uuid": "^3.0.1"
@@ -58,11 +58,12 @@
     "test-live": "NOCK_OFF=true mocha",
     "test-live-verbose": "env DEBUG='*,-mocha:*' npm run test-live",
     "tslint": "tslint --project types",
-    "type-check": "tsc --project types/tsconfig.json"
+    "type-check": "tsc --project types/tsconfig.json",
+    "mocha": "mocha"
   },
   "main": "./cloudant.js",
   "types": "types",
   "engines": {
-    "node": ">=6.13.0"
+    "node": ">=12"
   }
 }
diff --git a/plugins/cookieauth.js b/plugins/cookieauth.js
index 6a1f989..96fad87 100644
--- a/plugins/cookieauth.js
+++ b/plugins/cookieauth.js
@@ -1,4 +1,4 @@
-// Copyright © 2015, 2019 IBM Corp. All rights reserved.
+// Copyright © 2015, 2021 IBM Corp. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -68,16 +68,13 @@ class CookiePlugin extends BasePlugin {
     delete req.url;
     req.uri = u.format(new u.URL(req.uri), {auth: false});
 
-    self._tokenManager.renewIfRequired().then(() => {
-      callback(state);
-    }).catch((error) => {
+    self._tokenManager.renewIfRequired().catch((error) => {
       if (state.attempt < state.maxAttempt) {
         state.retry = true;
       } else {
         state.abortWithResponse = [ error ]; // return error to client
       }
-      callback(state);
-    });
+    }).finally(() => callback(state));
   }
 
   onResponse(state, response, callback) {
diff --git a/plugins/iamauth.js b/plugins/iamauth.js
index 59181f3..7e3ea73 100644
--- a/plugins/iamauth.js
+++ b/plugins/iamauth.js
@@ -1,4 +1,4 @@
-// Copyright © 2017, 2019 IBM Corp. All rights reserved.
+// Copyright © 2017, 2021 IBM Corp. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -66,9 +66,7 @@ class IAMPlugin extends BasePlugin {
     delete req.url;
     req.uri = u.format(new u.URL(req.uri), {auth: false});
 
-    self._tokenManager.renewIfRequired().then(() => {
-      callback(state);
-    }).catch((error) => {
+    self._tokenManager.renewIfRequired().catch((error) => {
       debug(error);
       if (state.attempt < state.maxAttempt) {
         state.retry = true;
@@ -85,8 +83,7 @@ class IAMPlugin extends BasePlugin {
       } else {
         state.abortWithResponse = [ error ]; // return error to client
       }
-      callback(state);
-    });
+    }).finally(() => callback(state));
   }
 
   onResponse(state, response, callback) {


Test patch:
diff --git a/test/mocha.opts b/test/mocha.opts
deleted file mode 100644
index 67dfc20..0000000
--- a/test/mocha.opts
+++ /dev/null
@@ -1,3 +0,0 @@
---exclude test/typescript/*.js
---recursive
---timeout 60000
diff --git a/test/plugins/iamauth.js b/test/plugins/iamauth.js
index ac8057f..3930af8 100644
--- a/test/plugins/iamauth.js
+++ b/test/plugins/iamauth.js
@@ -1,4 +1,4 @@
-// Copyright © 2017, 2019 IBM Corp. All rights reserved.
+// Copyright © 2017, 2021 IBM Corp. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -67,6 +67,9 @@ const MOCK_IAM_SESSION = 'IAMSession=dFNyccpWQd1iZF0zWTCwFDEEFTESPk8X21cygDdoe1R
 const MOCK_SET_IAM_SESSION_HEADER = {
   'set-cookie': `${MOCK_IAM_SESSION}; Version=1; Max-Age=3599; Secure; Path=/; HttpOnly; Secure`
 };
+const MOCK_SET_IAM_SESSION_HEADER_SHORT = {
+  'set-cookie': `${MOCK_IAM_SESSION}; Version=1; Max-Age=2; Secure; Path=/; HttpOnly; Secure`
+};
 
 describe('#db IAMAuth Plugin', function() {
   beforeEach(function() {
@@ -315,6 +318,84 @@ describe('#db IAMAuth Plugin', function() {
     });
   });
 
+  it('iam_session returns error', function() {
+    if (process.env.NOCK_OFF) {
+      this.skip();
+    }
+
+    var iamMocks = nock(TOKEN_SERVER)
+      .post('/identity/token', {
+        'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
+        'response_type': 'cloud_iam',
+        'apikey': IAM_API_KEY
+      })
+      .times(3)
+      .reply(200, MOCK_IAM_TOKEN_RESPONSE);
+
+    var cloudantMocks = nock(SERVER)
+      .post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
+      .times(3)
+      .replyWithError({code: 'ECONNRESET', message: 'socket hang up'});
+
+    var cloudantClient = new Cloudant({ url: SERVER, plugins: { iamauth: { autoRenew: false, iamApiKey: IAM_API_KEY } } });
+    return assert.rejects(
+      cloudantClient.db.get(DBNAME),
+      {
+        code: 'ECONNRESET',
+        description: 'socket hang up'
+      },
+      'Should have rejected with a socket hang up error').finally(() => {
+      iamMocks.done();
+      cloudantMocks.done();
+    });
+  });
+
+  it('iam_session returns error on renew', function() {
+    if (process.env.NOCK_OFF) {
+      this.skip();
+    }
+
+    var iamMocks = nock(TOKEN_SERVER)
+      .post('/identity/token', {
+        'grant_type': 'urn:ibm:params:oauth:grant-type:apikey',
+        'response_type': 'cloud_iam',
+        'apikey': IAM_API_KEY
+      })
+      .times(2)
+      .reply(200, MOCK_IAM_TOKEN_RESPONSE);
+
+    var cloudantMocks = nock(SERVER)
+      .post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
+      .reply(200, {ok: true}, MOCK_SET_IAM_SESSION_HEADER_SHORT)
+      .get(DBNAME)
+      .reply(200, {doc_count: 0})
+      .post('/_iam_session', {access_token: MOCK_ACCESS_TOKEN})
+      .replyWithError({code: 'ECONNRESET', message: 'socket hang up'})
+      .get(DBNAME)
+      .reply(401, {error: 'unauthorized', reason: 'Unauthorized'});
+
+    var cloudantClient = new Cloudant({ url: SERVER, maxAttempt: 1, plugins: { iamauth: { iamApiKey: IAM_API_KEY } } });
+    return cloudantClient.db.get(DBNAME.substring(1)) /* Remove leading slash */
+      .then(() => {
+        // Wait for long enough for the background renewal to fail and the token to expire
+        return new Promise(resolve => setTimeout(resolve, 2100));
+      })
+      .then(() => {
+        return assert.rejects(
+          cloudantClient.db.get(DBNAME.substring(1)),
+          {
+            error: 'unauthorized',
+            reason: 'Unauthorized',
+            statusCode: 401
+          },
+          'Should have rejected unauthorized after background renewal failure');
+      })
+      .finally(() => {
+        iamMocks.done();
+        cloudantMocks.done();
+      });
+  });
+
   it('retries access token post on error and returns 200 response', function(done) {
     if (process.env.NOCK_OFF) {
       this.skip();