From fb5492ba7d18c3d6c3bc8599463ede9984f257df Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 7 Oct 2025 10:49:34 +0000 Subject: [PATCH 1/7] Fix: Transform network connectivity errors into TLS timeout ApiError Transforms raw network errors (ECONNRESET, ETIMEDOUT, timed out, and TLS handshake) into a specific ApiError (code 408) with a descriptive message regarding potential CPU starvation. This prevents misleading error propagation from the underlying request library. --- src/nodejs-common/util.ts | 19 +++++ test/nodejs-common/util.ts | 144 +++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index aec0b2f50..07c5cfdc6 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -912,6 +912,25 @@ export class Util { options, // eslint-disable-next-line @typescript-eslint/no-explicit-any (err: Error | null, response: {}, body: any) => { + // Check for a TLS handshake timeout. + // This is a conceptual check, the exact error format may vary. + if ( + err && + (err.message?.includes('TLS handshake') || + err.message?.includes('timed out') || + err.message?.includes('ETIMEDOUT') || + err.message?.includes('ECONNRESET')) + ) { + // Create and use your custom error type. + const tlsTimeoutError = new ApiError({ + code: 408, + message: + 'TLS handshake timeout. This may be due to CPU starvation.', + response: response as r.Response, + }); + // Replace the original error with the more descriptive one. + err = tlsTimeoutError; + } util.handleResp(err, response as {} as r.Response, body, callback!); } ); diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index 3efc73d11..ac114b012 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -49,6 +49,7 @@ import { } from '../../src/nodejs-common/util.js'; import {DEFAULT_PROJECT_ID_TOKEN} from '../../src/nodejs-common/service.js'; import duplexify from 'duplexify'; +import {EventEmitter, Writable} from 'stream'; nock.disableNetConnect(); @@ -1189,6 +1190,100 @@ describe('common/util', () => { }); }); + describe('TLS handshake errors', () => { + const error = new Error('🤮'); + + beforeEach(() => { + authClient.authorizeRequest = async () => { + throw error; + }; + }); + + it('should transform raw ECONNRESET into TLS ApiError', done => { + const networkError = new Error('ECONNRESET'); + sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); + + createMakeRequestStub(sandbox, networkError, util, authClient); + const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( + {} + ); + + makeAuthenticatedRequest({} as DecorateRequestOptions, err => { + assert.ok(err); + assert.strictEqual((err as ApiError).code, 408); + assert.strictEqual( + (err as ApiError).message, + 'TLS handshake timeout. This may be due to CPU starvation.' + ); + done(); + }); + }); + + it('should transform raw "TLS handshake" into TLS ApiError', done => { + const networkError = new Error( + 'Request failed due to TLS handshake timeout.' + ); + sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); + createMakeRequestStub(sandbox, networkError, util, authClient); + + const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( + {} + ); + + makeAuthenticatedRequest({} as DecorateRequestOptions, err => { + assert.ok(err); + assert.strictEqual((err as ApiError).code, 408); + assert.strictEqual( + (err as ApiError).message, + 'TLS handshake timeout. This may be due to CPU starvation.' + ); + done(); + }); + }); + + it('should transform raw generic "timed out" into TLS ApiError', done => { + const networkError = new Error('The request timed out.'); + sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); + createMakeRequestStub(sandbox, networkError, util, authClient); + + const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( + {} + ); + + makeAuthenticatedRequest({} as DecorateRequestOptions, err => { + assert.ok(err); + assert.strictEqual((err as ApiError).code, 408); + assert.strictEqual( + (err as ApiError).message, + 'TLS handshake timeout. This may be due to CPU starvation.' + ); + done(); + }); + }); + + it('should transform raw ETIMEDOUT into TLS ApiError', done => { + const networkError = new Error( + 'Request failed with error: ETIMEDOUT' + ); + sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); + createMakeRequestStub(sandbox, networkError, util, authClient); + + const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( + {} + ); + + makeAuthenticatedRequest({} as DecorateRequestOptions, err => { + assert.ok(err); + assert.strictEqual((err as ApiError).code, 408); + assert.strictEqual( + (err as ApiError).message, + 'TLS handshake timeout. This may be due to CPU starvation.' + ); + done(); + }); + }); + }); + describe('authentication success', () => { const reqOpts = fakeReqOpts; beforeEach(() => { @@ -1891,3 +1986,52 @@ describe('common/util', () => { }); }); }); +function createMakeRequestStub( + sandbox: sinon.SinonSandbox, + networkError: Error, + util: Util & {[index: string]: Function}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authClient: any +) { + const authorizedReqOpts = {uri: 'test-uri'} as DecorateRequestOptions; + sandbox.stub(authClient, 'authorizeRequest').resolves(authorizedReqOpts); + sandbox.stub(authClient, 'getProjectId').resolves('test-project-id'); + + sandbox + .stub(util, 'makeRequest') + .callsFake((_authenticatedReqOpts, cfg, callback) => { + const mockRequestStream = new EventEmitter() as unknown as Writable & { + abort: () => void; + }; + mockRequestStream.abort = () => {}; + + if (!cfg.stream) { + const retryCallback = ( + err: Error | null, + response: {}, + body: unknown + ) => { + if ( + err && + (err.message?.includes('TLS handshake') || + err.message?.includes('timed out') || + err.message?.includes('ETIMEDOUT') || + err.message?.includes('ECONNRESET')) + ) { + const tlsTimeoutError = new ApiError({ + code: 408, + message: + 'TLS handshake timeout. This may be due to CPU starvation.', + response: response as r.Response, + }); + err = tlsTimeoutError; + } + util.handleResp(err, response as r.Response, body, callback!); + }; + + retryCallback(networkError, {} as r.Response, null); + } + + return mockRequestStream; + }); +} From 0b83843e2cbfdd780e30379ace9d685d347e8a78 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 7 Oct 2025 11:29:41 +0000 Subject: [PATCH 2/7] fix --- test/nodejs-common/util.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index ac114b012..d7858d0f8 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -49,7 +49,6 @@ import { } from '../../src/nodejs-common/util.js'; import {DEFAULT_PROJECT_ID_TOKEN} from '../../src/nodejs-common/service.js'; import duplexify from 'duplexify'; -import {EventEmitter, Writable} from 'stream'; nock.disableNetConnect(); @@ -1986,6 +1985,7 @@ describe('common/util', () => { }); }); }); + function createMakeRequestStub( sandbox: sinon.SinonSandbox, networkError: Error, @@ -2000,9 +2000,12 @@ function createMakeRequestStub( sandbox .stub(util, 'makeRequest') .callsFake((_authenticatedReqOpts, cfg, callback) => { - const mockRequestStream = new EventEmitter() as unknown as Writable & { - abort: () => void; - }; + const mockRequestStream = new stream.Duplex({ + write(_chunk, _encoding, callback) { + callback(); + }, + read() {}, + }) as stream.Duplex & {abort: () => void}; mockRequestStream.abort = () => {}; if (!cfg.stream) { From 942dff32d3bcadec7b0c4569d0875c12e7d4a1c4 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Thu, 16 Oct 2025 09:30:52 +0000 Subject: [PATCH 3/7] fix: Differentiate retry codes for network errors Splits network error handling: uses 408 for timeouts (timed out, ETIMEDOUT, TLS handshake) and 503 for connection resets (ECONNRESET) to improve retry logic accuracy. --- src/nodejs-common/util.ts | 27 ++++++++++++++++++++------- test/nodejs-common/util.ts | 34 ++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index 07c5cfdc6..9a65950db 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -916,16 +916,29 @@ export class Util { // This is a conceptual check, the exact error format may vary. if ( err && - (err.message?.includes('TLS handshake') || - err.message?.includes('timed out') || - err.message?.includes('ETIMEDOUT') || - err.message?.includes('ECONNRESET')) + (err.message?.toLowerCase().includes('tls handshake') || + err.message?.toLowerCase().includes('timed out') || + err.message?.toLowerCase().includes('etimedout') || + err.message?.toLowerCase().includes('econnreset')) ) { + let code: number; + let message: string; + if (err.message.toLowerCase().includes('econnreset')) { + // ECONNRESET (Connection reset by peer) implies temporary service unavailability + code = 503; + message = + 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.'; + } else { + // TLS Handshake, ETIMEDOUT, or generic 'timed out' + // Mapped to 408 (Request Timeout) as a signal for retrying + code = 408; + message = + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; + } // Create and use your custom error type. const tlsTimeoutError = new ApiError({ - code: 408, - message: - 'TLS handshake timeout. This may be due to CPU starvation.', + code, + message, response: response as r.Response, }); // Replace the original error with the more descriptive one. diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index d7858d0f8..87c7d1e33 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -1209,10 +1209,10 @@ describe('common/util', () => { makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual((err as ApiError).code, 408); + assert.strictEqual((err as ApiError).code, 503); assert.strictEqual( (err as ApiError).message, - 'TLS handshake timeout. This may be due to CPU starvation.' + 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.' ); done(); }); @@ -1234,7 +1234,7 @@ describe('common/util', () => { assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( (err as ApiError).message, - 'TLS handshake timeout. This may be due to CPU starvation.' + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); }); @@ -1254,7 +1254,7 @@ describe('common/util', () => { assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( (err as ApiError).message, - 'TLS handshake timeout. This may be due to CPU starvation.' + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); }); @@ -1276,7 +1276,7 @@ describe('common/util', () => { assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( (err as ApiError).message, - 'TLS handshake timeout. This may be due to CPU starvation.' + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); }); @@ -2016,15 +2016,25 @@ function createMakeRequestStub( ) => { if ( err && - (err.message?.includes('TLS handshake') || - err.message?.includes('timed out') || - err.message?.includes('ETIMEDOUT') || - err.message?.includes('ECONNRESET')) + (err.message?.toLowerCase().includes('tls handshake') || + err.message?.toLowerCase().includes('timed out') || + err.message?.toLowerCase().includes('etimedout') || + err.message?.toLowerCase().includes('econnreset')) ) { + let code: number; + let message: string; + if (err.message.toLowerCase().includes('econnreset')) { + code = 503; + message = + 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.'; + } else { + code = 408; + message = + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; + } const tlsTimeoutError = new ApiError({ - code: 408, - message: - 'TLS handshake timeout. This may be due to CPU starvation.', + code, + message, response: response as r.Response, }); err = tlsTimeoutError; From 0d43c770d6cd87ae96e6fde45230b816e1013e58 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 27 Oct 2025 14:31:25 +0000 Subject: [PATCH 4/7] fix: Transform network errors into specific TLS/CPU starvation Error Converts raw ECONNRESET, ETIMEDOUT, and TLS handshake failures into a standard Error object with an informative message. This helps diagnose CPU starvation or misleading 401 errors. --- src/nodejs-common/util.ts | 41 ++----- test/nodejs-common/util.ts | 237 ++++++++++++++++++++++++------------- 2 files changed, 167 insertions(+), 111 deletions(-) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index 9a65950db..3d8f243aa 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -912,37 +912,18 @@ export class Util { options, // eslint-disable-next-line @typescript-eslint/no-explicit-any (err: Error | null, response: {}, body: any) => { - // Check for a TLS handshake timeout. - // This is a conceptual check, the exact error format may vary. - if ( - err && - (err.message?.toLowerCase().includes('tls handshake') || - err.message?.toLowerCase().includes('timed out') || - err.message?.toLowerCase().includes('etimedout') || - err.message?.toLowerCase().includes('econnreset')) - ) { - let code: number; - let message: string; - if (err.message.toLowerCase().includes('econnreset')) { - // ECONNRESET (Connection reset by peer) implies temporary service unavailability - code = 503; - message = - 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.'; - } else { - // TLS Handshake, ETIMEDOUT, or generic 'timed out' - // Mapped to 408 (Request Timeout) as a signal for retrying - code = 408; - message = - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; + if (err) { + const lowerCaseMessage = err.message.toLowerCase(); + const isTLsTimeoutOrConnReset = + lowerCaseMessage.includes('tls handshake') || + lowerCaseMessage.includes('timed out') || + lowerCaseMessage.includes('etimedout') || + lowerCaseMessage.includes('econnreset'); + if (isTLsTimeoutOrConnReset) { + err = new Error( + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' + ); } - // Create and use your custom error type. - const tlsTimeoutError = new ApiError({ - code, - message, - response: response as r.Response, - }); - // Replace the original error with the more descriptive one. - err = tlsTimeoutError; } util.handleResp(err, response as {} as r.Response, body, callback!); } diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index 87c7d1e33..5d904e0ab 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -1190,18 +1190,66 @@ describe('common/util', () => { }); describe('TLS handshake errors', () => { - const error = new Error('🤮'); - + function createMakeRequestStub( + sandbox: sinon.SinonSandbox, + networkError: Error, + util: Util & {[index: string]: Function}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authClient: any + ) { + const authorizedReqOpts = {uri: 'test-uri'} as DecorateRequestOptions; + sandbox + .stub(authClient, 'authorizeRequest') + .resolves(authorizedReqOpts); + sandbox.stub(authClient, 'getProjectId').resolves('test-project-id'); + + sandbox + .stub(util, 'makeRequest') + .callsFake((_authenticatedReqOpts, cfg, callback) => { + const mockRequestStream = new stream.Duplex({ + write(_chunk, _encoding, callback) { + callback(); + }, + read() {}, + }) as stream.Duplex & {abort: () => void}; + mockRequestStream.abort = () => {}; + + if (!cfg.stream) { + const retryCallback = ( + err: Error | null, + response: {}, + body: unknown + ) => { + if (err) { + const lowerCaseMessage = err.message.toLowerCase(); + const isTLsTimeoutOrConnReset = + lowerCaseMessage.includes('tls handshake') || + lowerCaseMessage.includes('timed out') || + lowerCaseMessage.includes('etimedout') || + lowerCaseMessage.includes('econnreset'); + if (isTLsTimeoutOrConnReset) { + err = new Error( + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' + ); + } + } + util.handleResp(err, response as r.Response, body, callback!); + }; + + retryCallback(networkError, {} as r.Response, null); + } + + return mockRequestStream; + }); + } + const reqOpts = fakeReqOpts; beforeEach(() => { - authClient.authorizeRequest = async () => { - throw error; - }; + authClient.authorizeRequest = async () => reqOpts; }); it('should transform raw ECONNRESET into TLS ApiError', done => { const networkError = new Error('ECONNRESET'); sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); - createMakeRequestStub(sandbox, networkError, util, authClient); const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( {} @@ -1209,10 +1257,9 @@ describe('common/util', () => { makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual((err as ApiError).code, 503); assert.strictEqual( - (err as ApiError).message, - 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.' + err.message, + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); }); @@ -1224,16 +1271,14 @@ describe('common/util', () => { ); sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( {} ); makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( - (err as ApiError).message, + err.message, 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); @@ -1244,16 +1289,14 @@ describe('common/util', () => { const networkError = new Error('The request timed out.'); sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( {} ); makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( - (err as ApiError).message, + err.message, 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); @@ -1266,16 +1309,14 @@ describe('common/util', () => { ); sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( {} ); makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual((err as ApiError).code, 408); assert.strictEqual( - (err as ApiError).message, + err.message, 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); done(); @@ -1664,6 +1705,22 @@ describe('common/util', () => { }); describe('callback mode', () => { + function testTlsErrorTransformation( + errorToMock: Error | NodeJS.ErrnoException + ) { + const mockResponse: r.Response = { + headers: {}, + } as r.Response; + + return ( + _rOpts: DecorateRequestOptions, + _opts: MakeRequestConfig, + callback: r.RequestCallback + ) => { + callback(errorToMock, mockResponse, null); + }; + } + it('should pass the default options to retryRequest', done => { retryRequestOverride = testDefaultRetryRequestConfig(done); util.makeRequest( @@ -1741,6 +1798,87 @@ describe('common/util', () => { util.makeRequest(fakeReqOpts, {}, assert.ifError); }); + + it('should transform "tls handshake" into specific TLS/CPU starvation Error', done => { + const networkError = new Error('Request failed due to TLS handshake.'); + + // Stub handleResp to inspect the transformed error + stub('handleResp', err => { + assert.ok(err); + + // Assert the error transformation occurred + assert.notStrictEqual(err, networkError); + assert.strictEqual( + err.message, + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' + ); + done(); + }); + + retryRequestOverride = testTlsErrorTransformation(networkError); + util.makeRequest(reqOpts, {}, assert.ifError); + }); + + it('should transform "timed out" into specific TLS/CPU starvation Error', done => { + const networkError = new Error('Client request timed out.'); + + stub('handleResp', err => { + assert.ok(err); + assert.ok(err.message, 'TLS handshake timed out'); + done(); + }); + + retryRequestOverride = testTlsErrorTransformation(networkError); + util.makeRequest(reqOpts, {}, assert.ifError); + }); + + it('should transform "etimedout" into specific TLS/CPU starvation Error', done => { + const networkError: NodeJS.ErrnoException = new Error( + 'connect ETIMEDOUT' + ); + networkError.code = 'ETIMEDOUT'; + + stub('handleResp', err => { + assert.ok(err); + assert.ok(err.message, 'TLS handshake timed out'); + done(); + }); + + retryRequestOverride = testTlsErrorTransformation(networkError); + util.makeRequest(reqOpts, {}, assert.ifError); + }); + + it('should transform "econnreset" into specific TLS/CPU starvation Error', done => { + const networkError: NodeJS.ErrnoException = new Error( + 'socket ECONNRESET' + ); + networkError.code = 'ECONNRESET'; + + stub('handleResp', err => { + assert.ok(err); + assert.ok(err.message, 'TLS handshake timed out'); + done(); + }); + + retryRequestOverride = testTlsErrorTransformation(networkError); + util.makeRequest(reqOpts, {}, assert.ifError); + }); + + it('should NOT transform a regular non-TLS error', done => { + const networkError = new Error('Non-network API error.'); + + // Stub handleResp to check if the error is passed through unchanged + stub('handleResp', err => { + assert.ok(err); + // Assert the error was NOT transformed and is the original object + assert.strictEqual(err, networkError); + assert.doesNotMatch(err.message, /TLS handshake timed out/); + done(); + }); + + retryRequestOverride = testTlsErrorTransformation(networkError); + util.makeRequest(reqOpts, {}, assert.ifError); + }); }); }); @@ -1985,66 +2123,3 @@ describe('common/util', () => { }); }); }); - -function createMakeRequestStub( - sandbox: sinon.SinonSandbox, - networkError: Error, - util: Util & {[index: string]: Function}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authClient: any -) { - const authorizedReqOpts = {uri: 'test-uri'} as DecorateRequestOptions; - sandbox.stub(authClient, 'authorizeRequest').resolves(authorizedReqOpts); - sandbox.stub(authClient, 'getProjectId').resolves('test-project-id'); - - sandbox - .stub(util, 'makeRequest') - .callsFake((_authenticatedReqOpts, cfg, callback) => { - const mockRequestStream = new stream.Duplex({ - write(_chunk, _encoding, callback) { - callback(); - }, - read() {}, - }) as stream.Duplex & {abort: () => void}; - mockRequestStream.abort = () => {}; - - if (!cfg.stream) { - const retryCallback = ( - err: Error | null, - response: {}, - body: unknown - ) => { - if ( - err && - (err.message?.toLowerCase().includes('tls handshake') || - err.message?.toLowerCase().includes('timed out') || - err.message?.toLowerCase().includes('etimedout') || - err.message?.toLowerCase().includes('econnreset')) - ) { - let code: number; - let message: string; - if (err.message.toLowerCase().includes('econnreset')) { - code = 503; - message = - 'Connection reset by peer. This suggests the remote service is temporarily unavailable or overloaded.'; - } else { - code = 408; - message = - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; - } - const tlsTimeoutError = new ApiError({ - code, - message, - response: response as r.Response, - }); - err = tlsTimeoutError; - } - util.handleResp(err, response as r.Response, body, callback!); - }; - - retryCallback(networkError, {} as r.Response, null); - } - - return mockRequestStream; - }); -} From 286a6b2cb0bdb3d60ed7a0b96752c61129d701fe Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Mon, 27 Oct 2025 15:17:42 +0000 Subject: [PATCH 5/7] refactor: Combine duplicate TLS error tests into data-driven suite Replaces repetitive test cases in `makeAuthenticatedRequest` and `makeRequest` with a single, data-driven test loop. This verifies all conditions (ECONNRESET, ETIMEDOUT, "timed out", "TLS handshake") with reduced code duplication and improved maintenance. ``` --- src/nodejs-common/util.ts | 3 +- test/nodejs-common/util.ts | 262 ++++++------------------------------- 2 files changed, 43 insertions(+), 222 deletions(-) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index 3d8f243aa..44cfe5f3f 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -920,9 +920,10 @@ export class Util { lowerCaseMessage.includes('etimedout') || lowerCaseMessage.includes('econnreset'); if (isTLsTimeoutOrConnReset) { - err = new Error( + const timeOutError = new Error( 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' ); + err = timeOutError; } } util.handleResp(err, response as {} as r.Response, body, callback!); diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index 5d904e0ab..e6f49f106 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -1190,136 +1190,53 @@ describe('common/util', () => { }); describe('TLS handshake errors', () => { - function createMakeRequestStub( - sandbox: sinon.SinonSandbox, - networkError: Error, - util: Util & {[index: string]: Function}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - authClient: any - ) { - const authorizedReqOpts = {uri: 'test-uri'} as DecorateRequestOptions; - sandbox - .stub(authClient, 'authorizeRequest') - .resolves(authorizedReqOpts); - sandbox.stub(authClient, 'getProjectId').resolves('test-project-id'); - - sandbox - .stub(util, 'makeRequest') - .callsFake((_authenticatedReqOpts, cfg, callback) => { - const mockRequestStream = new stream.Duplex({ - write(_chunk, _encoding, callback) { - callback(); - }, - read() {}, - }) as stream.Duplex & {abort: () => void}; - mockRequestStream.abort = () => {}; - - if (!cfg.stream) { - const retryCallback = ( - err: Error | null, - response: {}, - body: unknown - ) => { - if (err) { - const lowerCaseMessage = err.message.toLowerCase(); - const isTLsTimeoutOrConnReset = - lowerCaseMessage.includes('tls handshake') || - lowerCaseMessage.includes('timed out') || - lowerCaseMessage.includes('etimedout') || - lowerCaseMessage.includes('econnreset'); - if (isTLsTimeoutOrConnReset) { - err = new Error( - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - } - } - util.handleResp(err, response as r.Response, body, callback!); - }; - - retryCallback(networkError, {} as r.Response, null); - } - - return mockRequestStream; - }); - } const reqOpts = fakeReqOpts; beforeEach(() => { authClient.authorizeRequest = async () => reqOpts; - }); - - it('should transform raw ECONNRESET into TLS ApiError', done => { - const networkError = new Error('ECONNRESET'); - sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); - createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( - {} - ); - - makeAuthenticatedRequest({} as DecorateRequestOptions, err => { - assert.ok(err); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - done(); - }); - }); - - it('should transform raw "TLS handshake" into TLS ApiError', done => { - const networkError = new Error( - 'Request failed due to TLS handshake timeout.' - ); sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); - createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( - {} - ); - - makeAuthenticatedRequest({} as DecorateRequestOptions, err => { - assert.ok(err); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - done(); - }); }); - it('should transform raw generic "timed out" into TLS ApiError', done => { - const networkError = new Error('The request timed out.'); - sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); - createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( - {} - ); - - makeAuthenticatedRequest({} as DecorateRequestOptions, err => { - assert.ok(err); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - done(); - }); - }); - - it('should transform raw ETIMEDOUT into TLS ApiError', done => { - const networkError = new Error( - 'Request failed with error: ETIMEDOUT' - ); - sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient); - createMakeRequestStub(sandbox, networkError, util, authClient); - const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory( - {} - ); - - makeAuthenticatedRequest({} as DecorateRequestOptions, err => { - assert.ok(err); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - done(); + const testCases = [ + {name: 'ECONNRESET', error: new Error('ECONNRESET')}, + { + name: '"TLS handshake"', + error: new Error('Request failed due to TLS handshake timeout.'), + }, + { + name: 'generic "timed out"', + error: new Error('The request timed out.'), + }, + { + name: 'ETIMEDOUT', + error: new Error('Request failed with error: ETIMEDOUT'), + }, + ]; + + testCases.forEach(({name, error: networkError}) => { + it(`should transform raw ${name} into specific TLS/CPU starvation Error`, done => { + // Override `retry-request` to simulate a network error. + retryRequestOverride = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _reqOpts: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _opts: any, + callback: (err: Error, res: {}, body: null) => void + ) => { + callback(networkError, {}, null); + return {abort: () => {}}; // Return an abortable request. + }; + + const makeAuthenticatedRequest = + util.makeAuthenticatedRequestFactory({}); + + makeAuthenticatedRequest({} as DecorateRequestOptions, err => { + assert.ok(err); + assert.strictEqual( + err.message, + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' + ); + done(); + }); }); }); }); @@ -1705,22 +1622,6 @@ describe('common/util', () => { }); describe('callback mode', () => { - function testTlsErrorTransformation( - errorToMock: Error | NodeJS.ErrnoException - ) { - const mockResponse: r.Response = { - headers: {}, - } as r.Response; - - return ( - _rOpts: DecorateRequestOptions, - _opts: MakeRequestConfig, - callback: r.RequestCallback - ) => { - callback(errorToMock, mockResponse, null); - }; - } - it('should pass the default options to retryRequest', done => { retryRequestOverride = testDefaultRetryRequestConfig(done); util.makeRequest( @@ -1798,87 +1699,6 @@ describe('common/util', () => { util.makeRequest(fakeReqOpts, {}, assert.ifError); }); - - it('should transform "tls handshake" into specific TLS/CPU starvation Error', done => { - const networkError = new Error('Request failed due to TLS handshake.'); - - // Stub handleResp to inspect the transformed error - stub('handleResp', err => { - assert.ok(err); - - // Assert the error transformation occurred - assert.notStrictEqual(err, networkError); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); - done(); - }); - - retryRequestOverride = testTlsErrorTransformation(networkError); - util.makeRequest(reqOpts, {}, assert.ifError); - }); - - it('should transform "timed out" into specific TLS/CPU starvation Error', done => { - const networkError = new Error('Client request timed out.'); - - stub('handleResp', err => { - assert.ok(err); - assert.ok(err.message, 'TLS handshake timed out'); - done(); - }); - - retryRequestOverride = testTlsErrorTransformation(networkError); - util.makeRequest(reqOpts, {}, assert.ifError); - }); - - it('should transform "etimedout" into specific TLS/CPU starvation Error', done => { - const networkError: NodeJS.ErrnoException = new Error( - 'connect ETIMEDOUT' - ); - networkError.code = 'ETIMEDOUT'; - - stub('handleResp', err => { - assert.ok(err); - assert.ok(err.message, 'TLS handshake timed out'); - done(); - }); - - retryRequestOverride = testTlsErrorTransformation(networkError); - util.makeRequest(reqOpts, {}, assert.ifError); - }); - - it('should transform "econnreset" into specific TLS/CPU starvation Error', done => { - const networkError: NodeJS.ErrnoException = new Error( - 'socket ECONNRESET' - ); - networkError.code = 'ECONNRESET'; - - stub('handleResp', err => { - assert.ok(err); - assert.ok(err.message, 'TLS handshake timed out'); - done(); - }); - - retryRequestOverride = testTlsErrorTransformation(networkError); - util.makeRequest(reqOpts, {}, assert.ifError); - }); - - it('should NOT transform a regular non-TLS error', done => { - const networkError = new Error('Non-network API error.'); - - // Stub handleResp to check if the error is passed through unchanged - stub('handleResp', err => { - assert.ok(err); - // Assert the error was NOT transformed and is the original object - assert.strictEqual(err, networkError); - assert.doesNotMatch(err.message, /TLS handshake timed out/); - done(); - }); - - retryRequestOverride = testTlsErrorTransformation(networkError); - util.makeRequest(reqOpts, {}, assert.ifError); - }); }); }); From fa8a54d0d33f0fe2c0e39e3bb4bfee99be46b322 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Tue, 28 Oct 2025 09:07:11 +0000 Subject: [PATCH 6/7] Code refactor --- src/nodejs-common/util.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index 44cfe5f3f..017687840 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -915,14 +915,13 @@ export class Util { if (err) { const lowerCaseMessage = err.message.toLowerCase(); const isTLsTimeoutOrConnReset = - lowerCaseMessage.includes('tls handshake') || - lowerCaseMessage.includes('timed out') || - lowerCaseMessage.includes('etimedout') || - lowerCaseMessage.includes('econnreset'); - if (isTLsTimeoutOrConnReset) { - const timeOutError = new Error( - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' + /tls handshake|timed out|etimedout|econnreset/.test( + lowerCaseMessage ); + if (isTLsTimeoutOrConnReset) { + const TLS_TIMEOUT_ERROR_MESSAGE = + 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; + const timeOutError = new Error(TLS_TIMEOUT_ERROR_MESSAGE); err = timeOutError; } } From 264beb528ae7aa86103f90fce7c405fefcfe4b52 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Wed, 10 Dec 2025 08:49:04 +0000 Subject: [PATCH 7/7] Fix: Improve error handling to distinguish network errors from TLS failures Separates specific network transport errors (`ETIMEDOUT`, `ECONNRESET`) from genuine TLS handshake failures. The previous approach incorrectly categorized lower-level connection issues as "TLS errors," leading to misleading diagnostics for end-users. This change ensures accurate reporting based on the error pattern: - "tls handshake": Protocol/certificate issue. - "etimedout" / "timed out": Network timeout/availability. - "econnreset": Connection forcefully reset by host/intermediary. --- src/nodejs-common/util.ts | 36 ++++++++++++++++++++++++++++-------- test/nodejs-common/util.ts | 21 +++++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/nodejs-common/util.ts b/src/nodejs-common/util.ts index 017687840..d4425e76c 100644 --- a/src/nodejs-common/util.ts +++ b/src/nodejs-common/util.ts @@ -256,6 +256,12 @@ export interface ParsedHttpResponseBody { err?: Error; } +export enum UtilExceptionMessages { + TLS_TIMEOUT_ERROR_MESSAGE = 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.', + ETIMEDOUT_ERROR_MESSAGE = 'Connection timed out. This suggests a network issue or the server took too long to respond.', + ECONNRESET_ERROR_MESSAGE = 'Connection reset by peer. The connection was closed unexpectedly, possibly by a firewall or network issue.', +} + /** * Custom error type for API errors. * @@ -914,15 +920,29 @@ export class Util { (err: Error | null, response: {}, body: any) => { if (err) { const lowerCaseMessage = err.message.toLowerCase(); - const isTLsTimeoutOrConnReset = - /tls handshake|timed out|etimedout|econnreset/.test( - lowerCaseMessage + let newError: Error | undefined; + + if (lowerCaseMessage.includes('tls handshake')) { + newError = new Error( + UtilExceptionMessages.TLS_TIMEOUT_ERROR_MESSAGE + ); + } else if ( + lowerCaseMessage.includes('etimedout') || + lowerCaseMessage.includes('timed out') + ) { + newError = new Error( + UtilExceptionMessages.ETIMEDOUT_ERROR_MESSAGE + ); + } else if (lowerCaseMessage.includes('econnreset')) { + newError = new Error( + UtilExceptionMessages.ECONNRESET_ERROR_MESSAGE ); - if (isTLsTimeoutOrConnReset) { - const TLS_TIMEOUT_ERROR_MESSAGE = - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.'; - const timeOutError = new Error(TLS_TIMEOUT_ERROR_MESSAGE); - err = timeOutError; + } + + if (newError) { + // Preserve the original stack trace for better debugging + newError.stack = err.stack; + err = newError; } } util.handleResp(err, response as {} as r.Response, body, callback!); diff --git a/test/nodejs-common/util.ts b/test/nodejs-common/util.ts index e6f49f106..58c107f35 100644 --- a/test/nodejs-common/util.ts +++ b/test/nodejs-common/util.ts @@ -46,6 +46,7 @@ import { MakeRequestConfig, ParsedHttpRespMessage, Util, + UtilExceptionMessages, } from '../../src/nodejs-common/util.js'; import {DEFAULT_PROJECT_ID_TOKEN} from '../../src/nodejs-common/service.js'; import duplexify from 'duplexify'; @@ -1189,7 +1190,7 @@ describe('common/util', () => { }); }); - describe('TLS handshake errors', () => { + describe('Handling of TLS Handshake, Timeout, and Connection Reset Errors in Authenticated Requests', () => { const reqOpts = fakeReqOpts; beforeEach(() => { authClient.authorizeRequest = async () => reqOpts; @@ -1197,23 +1198,30 @@ describe('common/util', () => { }); const testCases = [ - {name: 'ECONNRESET', error: new Error('ECONNRESET')}, + { + name: 'ECONNRESET', + error: new Error('ECONNRESET'), + expectedMessage: UtilExceptionMessages.ECONNRESET_ERROR_MESSAGE, + }, { name: '"TLS handshake"', error: new Error('Request failed due to TLS handshake timeout.'), + expectedMessage: UtilExceptionMessages.TLS_TIMEOUT_ERROR_MESSAGE, }, { name: 'generic "timed out"', error: new Error('The request timed out.'), + expectedMessage: UtilExceptionMessages.ETIMEDOUT_ERROR_MESSAGE, }, { name: 'ETIMEDOUT', error: new Error('Request failed with error: ETIMEDOUT'), + expectedMessage: UtilExceptionMessages.ETIMEDOUT_ERROR_MESSAGE, }, ]; - testCases.forEach(({name, error: networkError}) => { - it(`should transform raw ${name} into specific TLS/CPU starvation Error`, done => { + testCases.forEach(({name, error: networkError, expectedMessage}) => { + it(`should transform raw ${name} into specific network error`, done => { // Override `retry-request` to simulate a network error. retryRequestOverride = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -1231,10 +1239,7 @@ describe('common/util', () => { makeAuthenticatedRequest({} as DecorateRequestOptions, err => { assert.ok(err); - assert.strictEqual( - err.message, - 'Request or TLS handshake timed out. This may be due to CPU starvation or a temporary network issue.' - ); + assert.strictEqual(err!.message, expectedMessage); done(); }); });