You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by st...@apache.org on 2021/10/21 09:10:25 UTC

[openwhisk-client-js] branch master updated: Support client retries (#227)

This is an automated email from the ASF dual-hosted git repository.

stanciu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk-client-js.git


The following commit(s) were added to refs/heads/master by this push:
     new 4dfc896  Support client retries (#227)
4dfc896 is described below

commit 4dfc896d230f2d705c300ee1520ef25b8008762d
Author: Moritz Raho <ra...@gmail.com>
AuthorDate: Thu Oct 21 11:10:17 2021 +0200

    Support client retries (#227)
    
    * Support client retries
    
    * typo
    
    * Update README.md with Rodric's suggestion
    
    Co-authored-by: rodric rabbah <ro...@gmail.com>
    
    * add no retry test + fix test nock race conditions
    
    * retry spy + test no retry on success
    
    * trigger travis build
    
    * install only production dependencies for nnewer npm versionns
    
    * back to old npm prod install + increase size too 2k
    
    * actually let s try to update the npm prod install
    
    Co-authored-by: rodric rabbah <ro...@gmail.com>
---
 README.md                |   2 +-
 lib/client.js            |  42 ++++++++++++-
 package-lock.json        | 160 ++++++++++++++++++++++++++++++++++++++++++++---
 package.json             |   4 +-
 test/unit/client.test.js |  82 +++++++++++++++++++++++-
 tools/check_size.sh      |   2 +-
 6 files changed, 276 insertions(+), 16 deletions(-)

diff --git a/README.md b/README.md
index a3a91ae..f581ab9 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ _Client constructor supports the following mandatory parameters:_
 - **key**. Client key to use when connecting to the `apihost` (if `nginx_ssl_verify_client` is turned on in your apihost)
 - **proxy.** HTTP(s) URI for proxy service to forwards requests through. Uses Needle's [built-in proxy support](https://github.com/tomas/needle#request-options).
 - **agent.** Provide custom [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent) implementation.
-
+- **retry**. Provide a retry options to retry on errors, for example, `{ retries: 2 }`. By default, no retries will be done. Uses [async-retry options](https://github.com/vercel/async-retry#api). Default values are different from async-retry, please refer to the API doc.
 
 ### environment variables
 
diff --git a/lib/client.js b/lib/client.js
index e75bf40..ee92620 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -22,6 +22,7 @@ const OpenWhiskError = require('./openwhisk_error')
 const needle = require('needle')
 const url = require('url')
 const http = require('http')
+const retry = require('async-retry')
 
 /**
  * This implements a request-promise-like facade over the needle
@@ -77,6 +78,13 @@ const rp = opts => {
     })
 }
 
+const rpWithRetry = opts => {
+  return retry(bail => {
+    // will retry on exception
+    return rp(opts)
+  }, opts.retry)
+}
+
 class Client {
   /**
    * @constructor
@@ -93,6 +101,13 @@ class Client {
    * @param {boolean} [options.noUserAgent]
    * @param {string} [options.cert]
    * @param {string} [options.key]
+   * @param {object} [options.retry]
+   * @param {number} [options.retry.retries] Number of retries on top of the initial request, default is 2.
+   * @param {number} [options.retry.factor] Exponential factor, default is 2.
+   * @param {number} [options.retry.minTimeout] Milliseconds before the first retry, default is 100.
+   * @param {number} [options.retry.maxTimeout] Max milliseconds in between two retries, default is infinity.
+   * @param {boolean} [options.retry.randomize] Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is true.
+   * @param {Function} [options.retry.onRetry] An optional function that is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter.
    */
   constructor (options) {
     this.options = this.parseOptions(options || {})
@@ -135,7 +150,21 @@ class Client {
       throw new Error(`${messages.INVALID_OPTIONS_ERROR} Missing either api or apihost parameters.`)
     }
 
-    return { apiKey: apiKey, api, apiVersion: apiversion, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler, noUserAgent: options.noUserAgent, cert: options.cert, key: options.key, proxy, agent }
+    // gather retry options
+    const retry = options.retry
+    if (retry && typeof options.retry !== 'object') {
+      throw new Error(`${messages.INVALID_OPTIONS_ERROR} 'retry' option must be an object, e.g. '{ retries: 2 }'.`)
+    }
+    if (retry) {
+      // overwrite async-retry defaults, see https://github.com/vercel/async-retry#api for more details
+      retry.retries = retry.retries || 2
+      retry.factor = retry.factor || 2
+      retry.minTimeout = retry.minTimeout || 100
+      retry.maxTimeout = retry.maxTimeout || Infinity
+      retry.randomize = retry.randomize || true
+    }
+
+    return { apiKey: apiKey, api, apiVersion: apiversion, ignoreCerts: ignoreCerts, namespace: options.namespace, apigwToken: apigwToken, apigwSpaceGuid: apigwSpaceGuid, authHandler: options.auth_handler, noUserAgent: options.noUserAgent, cert: options.cert, key: options.key, proxy, agent, retry }
   }
 
   urlFromApihost (apihost, apiversion = 'v1') {
@@ -152,7 +181,12 @@ class Client {
 
   request (method, path, options) {
     const params = this.params(method, path, options)
-    return params.then(req => rp(req)).catch(err => this.handleErrors(err))
+    return params.then(req => {
+      if (req.retry) {
+        return rpWithRetry(req)
+      }
+      return rp(req)
+    }).catch(err => this.handleErrors(err))
   }
 
   params (method, path, options) {
@@ -194,6 +228,10 @@ class Client {
         parms.agent = this.options.agent
       }
 
+      if (this.options.retry) {
+        parms.retry = this.options.retry
+      }
+
       return parms
     })
   }
diff --git a/package-lock.json b/package-lock.json
index 8bb56ed..3994504 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -455,6 +455,41 @@
       "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
       "dev": true
     },
+    "@sinonjs/commons": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
+      "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
+      "dev": true,
+      "requires": {
+        "type-detect": "4.0.8"
+      }
+    },
+    "@sinonjs/fake-timers": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
+      "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0"
+      }
+    },
+    "@sinonjs/samsam": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz",
+      "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.6.0",
+        "lodash.get": "^4.4.2",
+        "type-detect": "^4.0.8"
+      }
+    },
+    "@sinonjs/text-encoding": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz",
+      "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
+      "dev": true
+    },
     "@szmarczak/http-timer": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
@@ -724,6 +759,14 @@
       "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
       "dev": true
     },
+    "async-retry": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+      "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+      "requires": {
+        "retry": "0.13.1"
+      }
+    },
     "ava": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/ava/-/ava-2.4.0.tgz",
@@ -1581,14 +1624,6 @@
         "time-zone": "^1.0.0"
       }
     },
-    "debug": {
-      "version": "3.2.6",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
-      "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
-      "requires": {
-        "ms": "^2.1.1"
-      }
-    },
     "debug-log": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz",
@@ -1762,6 +1797,12 @@
         }
       }
     },
+    "diff": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+      "dev": true
+    },
     "dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -1948,6 +1989,15 @@
             "which": "^1.2.9"
           }
         },
+        "debug": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
         "ignore": {
           "version": "4.0.6",
           "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -3567,6 +3617,12 @@
         "set-immediate-shim": "~1.0.1"
       }
     },
+    "just-extend": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
+      "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+      "dev": true
+    },
     "keyv": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -3651,6 +3707,12 @@
       "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
       "dev": true
     },
+    "lodash.get": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+      "dev": true
+    },
     "lodash.islength": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/lodash.islength/-/lodash.islength-4.0.1.tgz",
@@ -3895,6 +3957,16 @@
         "debug": "^3.2.6",
         "iconv-lite": "^0.4.4",
         "sax": "^1.2.4"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        }
       }
     },
     "nested-error-stacks": {
@@ -3909,6 +3981,19 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
+    "nise": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
+      "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.7.0",
+        "@sinonjs/fake-timers": "^7.0.4",
+        "@sinonjs/text-encoding": "^0.7.1",
+        "just-extend": "^4.0.2",
+        "path-to-regexp": "^1.7.0"
+      }
+    },
     "nock": {
       "version": "11.9.1",
       "resolved": "https://registry.npmjs.org/nock/-/nock-11.9.1.tgz",
@@ -4349,6 +4434,23 @@
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
       "dev": true
     },
+    "path-to-regexp": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+      "dev": true,
+      "requires": {
+        "isarray": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        }
+      }
+    },
     "path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -4905,6 +5007,11 @@
         "signal-exit": "^3.0.2"
       }
     },
+    "retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="
+    },
     "reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -5019,6 +5126,37 @@
       "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
       "dev": true
     },
+    "sinon": {
+      "version": "11.1.2",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
+      "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
+      "dev": true,
+      "requires": {
+        "@sinonjs/commons": "^1.8.3",
+        "@sinonjs/fake-timers": "^7.1.2",
+        "@sinonjs/samsam": "^6.0.2",
+        "diff": "^5.0.0",
+        "nise": "^5.1.0",
+        "supports-color": "^7.2.0"
+      },
+      "dependencies": {
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
     "slash": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5632,6 +5770,12 @@
         "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
+    },
     "type-fest": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
diff --git a/package.json b/package.json
index be9c689..f16c73d 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "coverage:integration": "nyc --no-clean --silent npm run test:integration",
     "coverage:report": "nyc report --reporter=lcov --reporter=text-summary",
     "coverage:upload": "codecov",
-    "check-deps-size": "./tools/check_size.sh 1100",
+    "check-deps-size": "./tools/check_size.sh 2000",
     "lint": "standard"
   },
   "repository": {
@@ -47,9 +47,11 @@
     "nock": "^11.7.0",
     "nyc": "^14.1.1",
     "pre-commit": "^1.2.2",
+    "sinon": "^11.1.2",
     "standard": "^12.0.1"
   },
   "dependencies": {
+    "async-retry": "^1.3.3",
     "needle": "^2.4.0"
   }
 }
diff --git a/test/unit/client.test.js b/test/unit/client.test.js
index 99f473d..7e93c60 100644
--- a/test/unit/client.test.js
+++ b/test/unit/client.test.js
@@ -21,19 +21,95 @@ const test = require('ava')
 const Client = require('../../lib/client')
 const http = require('http')
 const nock = require('nock')
+const sinon = require('sinon')
+
+// Note: All client.request tests have to come before any of the proxy tests, as they interfere
+
+test('should return response', async t => {
+  const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
+  const METHOD = 'GET'
+  // NOTE: paths must be different as tests are run in parallel and adding/removing nock
+  // interceptors for a same path will create race conditions.
+  const PATH = '/return/response'
+
+  const mock = nock('https://test_host').get(PATH).times(1).reply(200, 'all good')
+  const result = await client.request(METHOD, PATH, {})
+  t.is(result.toString(), 'all good')
+  mock.interceptors.forEach(nock.removeInterceptor)
+})
 
-// Note: this has to come before any of the proxy tests, as they interfere
 test('should handle http request errors', async t => {
   const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
   const METHOD = 'GET'
-  const PATH = '/some/path'
+  const PATH = '/handle/error'
 
-  nock('https://test_host').get(PATH).replyWithError('simulated error')
+  const mock = nock('https://test_host').get(PATH).times(1).replyWithError('simulated error')
   const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
   t.truthy(error.message)
   t.assert(error.message.includes('simulated error'))
+  mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should support retries on error', async t => {
+  const retrySpy = sinon.spy()
+  const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 2, onRetry: retrySpy } })
+  const METHOD = 'GET'
+  const PATH = '/retry/on/error'
+
+  const mock = nock('https://test_host')
+    .get(PATH).times(2).replyWithError('simulated error')
+    .get(PATH).times(1).reply(200, 'now all good')
+  const result = await client.request(METHOD, PATH, {})
+  t.is(result.toString(), 'now all good')
+  t.is(retrySpy.callCount, 2)
+  mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should not retry on success', async t => {
+  const retrySpy = sinon.spy()
+  const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 10, onRetry: retrySpy } })
+  const METHOD = 'GET'
+  const PATH = '/no/retry/on/sucess'
+
+  const mock = nock('https://test_host')
+    .get(PATH).times(1).reply(200, 'now all good')
+  const result = await client.request(METHOD, PATH, {})
+  t.is(result.toString(), 'now all good')
+  t.is(retrySpy.callCount, 0) // => no retries
+  mock.interceptors.forEach(nock.removeInterceptor)
 })
 
+test('should not retry when no retry config available', async t => {
+  const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '' })
+  const METHOD = 'GET'
+  const PATH = '/no/config/no/retry'
+
+  const mock = nock('https://test_host')
+    .get(PATH).times(1).replyWithError('simulated error')
+    .get(PATH).times(1).reply(200, 'now all good')
+  const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
+  t.truthy(error.message)
+  t.assert(error.message.includes('simulated error'))
+  mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+test('should handle errors even after retries', async t => {
+  const retrySpy = sinon.spy()
+  const client = new Client({ api_key: 'secret', apihost: 'test_host', proxy: '', retry: { retries: 2, onRetry: retrySpy } })
+  const METHOD = 'GET'
+  const PATH = '/handle/error/on/retry'
+
+  const mock = nock('https://test_host')
+    .get(PATH).times(3).replyWithError('simulated error')
+    .get(PATH).times(1).reply(200, 'not enough retries to come here')
+  const error = await t.throwsAsync(client.request(METHOD, PATH, {}))
+  t.truthy(error.message)
+  t.assert(error.message.includes('simulated error'))
+  mock.interceptors.forEach(nock.removeInterceptor)
+})
+
+// end client request tests
+
 test('should use default constructor options', t => {
   const client = new Client({ api_key: 'aaa', apihost: 'my_host' })
   t.false(client.options.ignoreCerts)
diff --git a/tools/check_size.sh b/tools/check_size.sh
index a2b07ac..a65b03f 100755
--- a/tools/check_size.sh
+++ b/tools/check_size.sh
@@ -33,7 +33,7 @@ cd $UNPACK_DIR
 
 tar -xzf openwhisk-*.tgz
 cd package
-npm install --production --silent
+npm install --only=production --silent
 cd node_modules
 NODE_MODULES_SIZE=$(du  -ks | cut -f 1)