Repository: codecov/uploader
Pull request #1203 (2023-10-02 22:31:19)

fix: add ETIMEDOUT
fixes https://github.com/codecov/uploader/issues/1180

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/codecov/uploader/pull/1203?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) Report
> Merging [#1203](https://app.codecov.io/gh/codecov/uploader/pull/1203?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) (c3a934d) into [main](https://app.codecov.io/gh/codecov/uploader/commit/4d20e99ace5e90983ffc4c510603eb19aef1aab9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) (4d20e99) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main    #1203   +/-   ##
=======================================
  Coverage   92.48%   92.48%           
=======================================
  Files          36       36           
  Lines        1357     1357           
  Branches      273      273           
=======================================
  Hits         1255     1255           
  Misses         70       70           
  Partials       32       32           
```

| [Flag](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | Coverage Δ | |
|---|---|---|
| [aarch64](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [aarch64-without-git](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [alpine](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [alpine-proxy](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [alpine-without-git](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [linux](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [linux-without-git](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [macos](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [macos-without-git](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `?` | |
| [windows](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `92.48% <ø> (ø)` | |
| [windows-without-git](https://app.codecov.io/gh/codecov/uploader/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/codecov/uploader/pull/1203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov) | Coverage Δ | |
|---|---|---|
| [src/helpers/web.ts](https://app.codecov.io/gh/codecov/uploader/pull/1203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=codecov#diff-c3JjL2hlbHBlcnMvd2ViLnRz) | `85.00% <ø> (ø)` | |


Patch:
diff --git a/src/helpers/web.ts b/src/helpers/web.ts
index f837e6d..7e5617f 100644
--- a/src/helpers/web.ts
+++ b/src/helpers/web.ts
@@ -63,10 +63,12 @@ async function requestWithRetry(
     return response
   } catch (error: unknown) {
     if (
-      ((error instanceof errors.UndiciError && error.code == 'ECONNRESET') ||
+      (
+        (error instanceof errors.UndiciError && error.code == 'ECONNRESET') ||
+        (error instanceof errors.UndiciError && error.code == 'ETIMEDOUT') ||
         error instanceof errors.ConnectTimeoutError ||
-        error instanceof errors.SocketError) &&
-      retryCount < maxRetries
+        error instanceof errors.SocketError
+      ) && retryCount < maxRetries
     ) {
       const backoffDelay = baseBackoffDelayMs * 2 ** retryCount
       await sleep(backoffDelay)


Test patch:
diff --git a/test/helpers/web.test.ts b/test/helpers/web.test.ts
index ce9e337..5788f97 100644
--- a/test/helpers/web.test.ts
+++ b/test/helpers/web.test.ts
@@ -403,6 +403,117 @@ describe('Web Helpers', () => {
       expect(response.resultURL.href).toStrictEqual('https://results.codecov.io/')
     });
   });
+
+  describe('Uploader should retry POST on ETIMEDOUT', () => {
+    it('should retry and return response data when ETIMEDOUT occurs once', async () => {
+      const envs: UploaderEnvs = {}
+      const args: UploaderArgs = {
+        flags: '',
+        slug: '',
+        upstream: '',
+      }
+      const error = new errors.UndiciError("conn reset error")
+      error.code = 'ETIMEDOUT'
+      uploadURL = 'http://codecov.io'
+      mockAgent.disableNetConnect()
+      mockClient = mockAgent.get(uploadURL)
+
+      mockClient.intercept({
+        method: 'POST',
+        path: `/upload/v4?package=uploader-${version}&token=${token}&hello`,
+      }).replyWithError(new errors.ConnectTimeoutError('timeout error')).times(1)
+
+      mockClient.intercept({
+        method: 'POST',
+        path: `/upload/v4?package=uploader-${version}&token=${token}&hello`,
+      }).replyWithError(error).times(1)
+
+      mockClient.intercept({
+        method: 'POST',
+        path: `/upload/v4?package=uploader-${version}&token=${token}&hello`,
+      }).reply(200, 'testPOSTHTTP')
+
+      const responseData = await uploadToCodecovPOST(new URL(uploadURL), token, query, source, envs, args);
+      try {
+        expect(responseData).toBe('testPOSTHTTP')
+      } catch (error) {
+        throw new Error(`${responseData} - ${error}`)
+      }
+    });
+
+    it('should fail with error if ETIMEDOUT happens 5 times', async () => {
+      const mockSleep = jest.spyOn(utilModule, 'sleep').mockResolvedValue(42)
+      const envs: UploaderEnvs = {}
+      const args: UploaderArgs = {
+        flags: '',
+        slug: '',
+        upstream: '',
+      }
+      const error = new errors.UndiciError("conn reset error")
+      error.code = 'ETIMEDOUT'
+      uploadURL = 'http://codecov.io'
+      mockAgent.disableNetConnect()
+      mockClient = mockAgent.get(uploadURL)
+
+      mockClient.intercept({
+        method: 'POST',
+        path: `/upload/v4?package=uploader-${version}&token=${token}&hello`,
+      }).replyWithError(error).times(5)
+
+      mockClient.intercept({
+        method: 'POST',
+        path: `/upload/v4?package=uploader-${version}&token=${token}&hello`,
+      }).reply(200, 'testPOSTHTTP')
+
+      try {
+        await uploadToCodecovPOST(new URL(uploadURL), token, query, source, envs, args);
+        expect(true).toBe(false)
+      } catch (error) {
+        expect(error).toBeInstanceOf(errors.UndiciError)
+        expect(mockSleep).toBeCalledTimes(4)
+      }
+    })
+  });
+
+  describe('Uploader should retry PUT on ETIMEDOUT', () => {
+    it('should retry and return response data when ETIMEDOUT occurs once', async () => {
+      // Replace the original setTimeout with fakeSetTimeout
+      const envs: UploaderEnvs = {}
+    const args: UploaderArgs = {
+      flags: '',
+      slug: '',
+      upstream: '',
+    }
+    jest.spyOn(console, 'log').mockImplementation(() => void {})
+    const postResults: PostResults = {
+      putURL: new URL('https://codecov.io'),
+      resultURL: new URL('https://results.codecov.io'),
+    }
+
+      const error = new errors.UndiciError("conn reset error")
+      error.code = 'ETIMEDOUT'
+      mockAgent.disableNetConnect()
+      mockClient = mockAgent.get(postResults.putURL.origin)
+
+      mockClient.intercept({
+        method: 'PUT',
+        path: `/`,
+      }).replyWithError(new errors.ConnectTimeoutError('timeout error')).times(1)
+
+      mockClient.intercept({
+        method: 'PUT',
+        path: `/`,
+      }).replyWithError(error).times(1)
+
+      mockClient.intercept({
+        method: 'PUT',
+        path: `/`,
+      }).reply(200, postResults.resultURL.href)
+
+      const response = await uploadToCodecovPUT(postResults, uploadFile, envs, args)
+      expect(response.resultURL.href).toStrictEqual('https://results.codecov.io/')
+    });
+  });
 })
 
 describe('displayChangelog()', () => {