Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 62b1c83

Browse files
committed
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.
1 parent b38b5d2 commit 62b1c83

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

src/nodejs-common/util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,25 @@ export class Util {
912912
options,
913913
// eslint-disable-next-line @typescript-eslint/no-explicit-any
914914
(err: Error | null, response: {}, body: any) => {
915+
// Check for a TLS handshake timeout.
916+
// This is a conceptual check, the exact error format may vary.
917+
if (
918+
err &&
919+
(err.message?.includes('TLS handshake') ||
920+
err.message?.includes('timed out') ||
921+
err.message?.includes('ETIMEDOUT') ||
922+
err.message?.includes('ECONNRESET'))
923+
) {
924+
// Create and use your custom error type.
925+
const tlsTimeoutError = new ApiError({
926+
code: 408,
927+
message:
928+
'TLS handshake timeout. This may be due to CPU starvation.',
929+
response: response as r.Response,
930+
});
931+
// Replace the original error with the more descriptive one.
932+
err = tlsTimeoutError;
933+
}
915934
util.handleResp(err, response as {} as r.Response, body, callback!);
916935
}
917936
);

test/nodejs-common/util.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
} from '../../src/nodejs-common/util.js';
5050
import {DEFAULT_PROJECT_ID_TOKEN} from '../../src/nodejs-common/service.js';
5151
import duplexify from 'duplexify';
52+
import {EventEmitter, Writable} from 'stream';
5253

5354
nock.disableNetConnect();
5455

@@ -1189,6 +1190,100 @@ describe('common/util', () => {
11891190
});
11901191
});
11911192

1193+
describe('TLS handshake errors', () => {
1194+
const error = new Error('🤮');
1195+
1196+
beforeEach(() => {
1197+
authClient.authorizeRequest = async () => {
1198+
throw error;
1199+
};
1200+
});
1201+
1202+
it('should transform raw ECONNRESET into TLS ApiError', done => {
1203+
const networkError = new Error('ECONNRESET');
1204+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
1205+
1206+
createMakeRequestStub(sandbox, networkError, util, authClient);
1207+
const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory(
1208+
{}
1209+
);
1210+
1211+
makeAuthenticatedRequest({} as DecorateRequestOptions, err => {
1212+
assert.ok(err);
1213+
assert.strictEqual((err as ApiError).code, 408);
1214+
assert.strictEqual(
1215+
(err as ApiError).message,
1216+
'TLS handshake timeout. This may be due to CPU starvation.'
1217+
);
1218+
done();
1219+
});
1220+
});
1221+
1222+
it('should transform raw "TLS handshake" into TLS ApiError', done => {
1223+
const networkError = new Error(
1224+
'Request failed due to TLS handshake timeout.'
1225+
);
1226+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
1227+
createMakeRequestStub(sandbox, networkError, util, authClient);
1228+
1229+
const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory(
1230+
{}
1231+
);
1232+
1233+
makeAuthenticatedRequest({} as DecorateRequestOptions, err => {
1234+
assert.ok(err);
1235+
assert.strictEqual((err as ApiError).code, 408);
1236+
assert.strictEqual(
1237+
(err as ApiError).message,
1238+
'TLS handshake timeout. This may be due to CPU starvation.'
1239+
);
1240+
done();
1241+
});
1242+
});
1243+
1244+
it('should transform raw generic "timed out" into TLS ApiError', done => {
1245+
const networkError = new Error('The request timed out.');
1246+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
1247+
createMakeRequestStub(sandbox, networkError, util, authClient);
1248+
1249+
const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory(
1250+
{}
1251+
);
1252+
1253+
makeAuthenticatedRequest({} as DecorateRequestOptions, err => {
1254+
assert.ok(err);
1255+
assert.strictEqual((err as ApiError).code, 408);
1256+
assert.strictEqual(
1257+
(err as ApiError).message,
1258+
'TLS handshake timeout. This may be due to CPU starvation.'
1259+
);
1260+
done();
1261+
});
1262+
});
1263+
1264+
it('should transform raw ETIMEDOUT into TLS ApiError', done => {
1265+
const networkError = new Error(
1266+
'Request failed with error: ETIMEDOUT'
1267+
);
1268+
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
1269+
createMakeRequestStub(sandbox, networkError, util, authClient);
1270+
1271+
const makeAuthenticatedRequest = util.makeAuthenticatedRequestFactory(
1272+
{}
1273+
);
1274+
1275+
makeAuthenticatedRequest({} as DecorateRequestOptions, err => {
1276+
assert.ok(err);
1277+
assert.strictEqual((err as ApiError).code, 408);
1278+
assert.strictEqual(
1279+
(err as ApiError).message,
1280+
'TLS handshake timeout. This may be due to CPU starvation.'
1281+
);
1282+
done();
1283+
});
1284+
});
1285+
});
1286+
11921287
describe('authentication success', () => {
11931288
const reqOpts = fakeReqOpts;
11941289
beforeEach(() => {
@@ -1891,3 +1986,52 @@ describe('common/util', () => {
18911986
});
18921987
});
18931988
});
1989+
function createMakeRequestStub(
1990+
sandbox: sinon.SinonSandbox,
1991+
networkError: Error,
1992+
util: Util & {[index: string]: Function},
1993+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1994+
authClient: any
1995+
) {
1996+
const authorizedReqOpts = {uri: 'test-uri'} as DecorateRequestOptions;
1997+
sandbox.stub(authClient, 'authorizeRequest').resolves(authorizedReqOpts);
1998+
sandbox.stub(authClient, 'getProjectId').resolves('test-project-id');
1999+
2000+
sandbox
2001+
.stub(util, 'makeRequest')
2002+
.callsFake((_authenticatedReqOpts, cfg, callback) => {
2003+
const mockRequestStream = new EventEmitter() as unknown as Writable & {
2004+
abort: () => void;
2005+
};
2006+
mockRequestStream.abort = () => {};
2007+
2008+
if (!cfg.stream) {
2009+
const retryCallback = (
2010+
err: Error | null,
2011+
response: {},
2012+
body: unknown
2013+
) => {
2014+
if (
2015+
err &&
2016+
(err.message?.includes('TLS handshake') ||
2017+
err.message?.includes('timed out') ||
2018+
err.message?.includes('ETIMEDOUT') ||
2019+
err.message?.includes('ECONNRESET'))
2020+
) {
2021+
const tlsTimeoutError = new ApiError({
2022+
code: 408,
2023+
message:
2024+
'TLS handshake timeout. This may be due to CPU starvation.',
2025+
response: response as r.Response,
2026+
});
2027+
err = tlsTimeoutError;
2028+
}
2029+
util.handleResp(err, response as r.Response, body, callback!);
2030+
};
2031+
2032+
retryCallback(networkError, {} as r.Response, null);
2033+
}
2034+
2035+
return mockRequestStream;
2036+
});
2037+
}

0 commit comments

Comments
 (0)