From 47776ef15ef3590980c48d6dece1ea54657fdbca Mon Sep 17 00:00:00 2001 From: Bruno Pinto Date: Tue, 10 May 2022 00:47:36 +0100 Subject: [PATCH] feat(http-client): retry closed connection errors (#1336) * feat(http-client): Retry requests that failed with closed connection Requests that fail with closed connection errors (ECONNRESET, EPIPE) are automatically retried. - `ECONNRESET` (Connection reset by peer): A connection was forcibly closed by a peer.closed by a peer. This normally results from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered via the http and net modules. - `EPIPE` (Broken pipe): A write on a pipe, socket, or FIFO for which there is no process to read the data. Commonly encountered at the net and http layers, indicative that the remote side of the stream being written to has been closed. Fixes: #1040 --- lib/StripeResource.js | 12 ++++++++++-- lib/net/HttpClient.js | 1 + test/StripeResource.spec.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/StripeResource.js b/lib/StripeResource.js index ca364dae7e..0bfc93cd4e 100644 --- a/lib/StripeResource.js +++ b/lib/StripeResource.js @@ -273,7 +273,15 @@ StripeResource.prototype = { }, // For more on when and how to retry API requests, see https://stripe.com/docs/error-handling#safely-retrying-requests-with-idempotency - _shouldRetry(res, numRetries, maxRetries) { + _shouldRetry(res, numRetries, maxRetries, error) { + if ( + error && + numRetries === 0 && + HttpClient.CONNECTION_CLOSED_ERROR_CODES.includes(error.code) + ) { + return true; + } + // Do not retry if we are out of retries. if (numRetries >= maxRetries) { return false; @@ -529,7 +537,7 @@ StripeResource.prototype = { } }) .catch((error) => { - if (this._shouldRetry(null, requestRetries, maxRetries)) { + if (this._shouldRetry(null, requestRetries, maxRetries, error)) { return retryRequest( makeRequest, apiVersion, diff --git a/lib/net/HttpClient.js b/lib/net/HttpClient.js index eca551a084..053ab0d8e7 100644 --- a/lib/net/HttpClient.js +++ b/lib/net/HttpClient.js @@ -31,6 +31,7 @@ class HttpClient { } } +HttpClient.CONNECTION_CLOSED_ERROR_CODES = ['ECONNRESET', 'EPIPE']; HttpClient.TIMEOUT_ERROR_CODE = 'ETIMEDOUT'; class HttpClientResponse { diff --git a/test/StripeResource.spec.js b/test/StripeResource.spec.js index 23ab4def2b..1cdf98e285 100644 --- a/test/StripeResource.spec.js +++ b/test/StripeResource.spec.js @@ -403,6 +403,39 @@ describe('StripeResource', () => { ); }); + it('retries closed connection errors once', (done) => { + nock(`https://${options.host}`) + .post(options.path, options.params) + .replyWithError({ + code: 'ECONNRESET', + errno: 'ECONNRESET', + }) + .post(options.path, options.params) + .reply(200, { + id: 'ch_123', + object: 'charge', + amount: 1000, + }); + + realStripe.charges.create(options.data, (err, charge) => { + expect(charge.id).to.equal('ch_123'); + done(err); + }); + }); + + it('throws on multiple closed connection errors', (done) => { + nock(`https://${options.host}`) + .post(options.path, options.params) + .replyWithError({code: 'ECONNRESET'}) + .post(options.path, options.params) + .replyWithError({code: 'ECONNRESET'}); + + realStripe.charges.create(options.data, (err) => { + expect(err.detail.code).to.deep.equal('ECONNRESET'); + done(); + }); + }); + it('should retry the request if max retries are set', (done) => { nock(`https://${options.host}`) .post(options.path, options.params)