Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/nodejs-common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -912,6 +918,33 @@ export class Util {
options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(err: Error | null, response: {}, body: any) => {
if (err) {
const lowerCaseMessage = err.message.toLowerCase();
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 (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!);
}
);
Expand Down
57 changes: 57 additions & 0 deletions test/nodejs-common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1189,6 +1190,62 @@ describe('common/util', () => {
});
});

describe('Handling of TLS Handshake, Timeout, and Connection Reset Errors in Authenticated Requests', () => {
const reqOpts = fakeReqOpts;
beforeEach(() => {
authClient.authorizeRequest = async () => reqOpts;
sandbox.stub(fakeGoogleAuth, 'GoogleAuth').returns(authClient);
});

const testCases = [
{
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, 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
_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, expectedMessage);
done();
});
});
});
});

describe('authentication success', () => {
const reqOpts = fakeReqOpts;
beforeEach(() => {
Expand Down