From 3ea3b1aaea206a9fb90c3315da34174be92679c8 Mon Sep 17 00:00:00 2001 From: mithun50 Date: Sat, 4 Oct 2025 22:46:34 +0530 Subject: [PATCH] fix: prevent TypeError crashes on network errors with safe property access Fixed critical bug where accessing err.response.data without null checks caused TypeError crashes when network errors occurred (timeouts, DNS failures, connection refused). These errors don't have err.response property. Changes: - src/common/retryer.js: Use optional chaining for err.response.data access - src/fetchers/wakatime.js: Use optional chaining for err.response.status - tests/retryer.test.js: Add test coverage for network error scenarios Fixes crashes with "Cannot read property 'data' of undefined" and ensures graceful error handling during network instability. --- src/common/retryer.js | 5 ++--- src/fetchers/wakatime.js | 3 ++- tests/retryer.test.js | 45 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/common/retryer.js b/src/common/retryer.js index 090865ed00c6d..88c9a834bfef1 100644 --- a/src/common/retryer.js +++ b/src/common/retryer.js @@ -63,10 +63,9 @@ const retryer = async (fetcher, variables, retries = 0) => { } catch (err) { // prettier-ignore // also checking for bad credentials if any tokens gets invalidated - const isBadCredential = err.response.data && err.response.data.message === "Bad credentials"; + const isBadCredential = err.response?.data?.message === "Bad credentials"; const isAccountSuspended = - err.response.data && - err.response.data.message === "Sorry. Your account was suspended."; + err.response?.data?.message === "Sorry. Your account was suspended."; if (isBadCredential || isAccountSuspended) { logger.log(`PAT_${retries + 1} Failed`); diff --git a/src/fetchers/wakatime.js b/src/fetchers/wakatime.js index f69d6ae498eef..558b550fc87d1 100644 --- a/src/fetchers/wakatime.js +++ b/src/fetchers/wakatime.js @@ -21,7 +21,8 @@ const fetchWakatimeStats = async ({ username, api_domain }) => { return data.data; } catch (err) { - if (err.response.status < 200 || err.response.status > 299) { + const status = err.response?.status; + if (status && (status < 200 || status > 299)) { throw new CustomError( `Could not resolve to a User with the login of '${username}'`, "WAKATIME_USER_NOT_FOUND", diff --git a/tests/retryer.test.js b/tests/retryer.test.js index 76630039d5017..91c80b466900b 100644 --- a/tests/retryer.test.js +++ b/tests/retryer.test.js @@ -76,4 +76,49 @@ describe("Test Retryer", () => { expect(err.message).toBe("Downtime due to GitHub API rate limiting"); } }); + + it("retryer should handle network errors without err.response gracefully", async () => { + const fetcherNetworkError = jest.fn(() => { + const error = new Error("ECONNREFUSED"); + // Simulate network error without response object + return Promise.reject(error); + }); + + const res = await retryer(fetcherNetworkError, {}); + + expect(fetcherNetworkError).toBeCalledTimes(1); + expect(res).toBeUndefined(); // err.response is undefined for network errors + }); + + it("retryer should retry on bad credentials error", async () => { + const fetcherBadCreds = jest.fn((_vars, _token, retries) => { + if (retries < 1) { + const error = new Error("Bad credentials"); + error.response = { data: { message: "Bad credentials" } }; + return Promise.reject(error); + } + return Promise.resolve({ data: "ok" }); + }); + + const res = await retryer(fetcherBadCreds, {}); + + expect(fetcherBadCreds).toBeCalledTimes(2); + expect(res).toStrictEqual({ data: "ok" }); + }); + + it("retryer should retry on account suspended error", async () => { + const fetcherSuspended = jest.fn((_vars, _token, retries) => { + if (retries < 1) { + const error = new Error("Account suspended"); + error.response = { data: { message: "Sorry. Your account was suspended." } }; + return Promise.reject(error); + } + return Promise.resolve({ data: "ok" }); + }); + + const res = await retryer(fetcherSuspended, {}); + + expect(fetcherSuspended).toBeCalledTimes(2); + expect(res).toStrictEqual({ data: "ok" }); + }); });