From 1500fe31b431aa62aa9d69712cbb8c58fe11c414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 4 May 2026 11:28:29 +0200 Subject: [PATCH] restructure retry loop to remove parallel response/error variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each catch path now decides for itself whether to rethrow or sleep and continue, so the loop body has a definite Response after the try/catch and there is no shared `error: unknown` slot that could in principle be thrown unset. Pulls the exponential delay into a tiny local helper to share between the two retry paths. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/gitlab/GitLabIntegration.ts | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 31b9bb1578..f917ac92d9 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -107,42 +107,45 @@ export class GitLabIntegration implements ScmIntegration { return fetchFn; } + // Exponential backoff, cap at 10 seconds + const backoffDelay = (a: number) => + Math.min(100 * Math.pow(2, a - 1), 10000); + return async (url, options) => { const abortSignal = options?.signal; let attempt = 0; for (;;) { - let response: Response | undefined; - let error: unknown; + let response: Response; try { response = await fetchFn(url, options); } catch (e) { // The caller aborted — surface that immediately rather than retrying. - if (abortSignal?.aborted) { - throw e; - } - error = e; + if (abortSignal?.aborted) throw e; + // No more attempts left — propagate the network error. + if (attempt++ >= maxRetries) throw e; + await sleep(backoffDelay(attempt), abortSignal); + continue; } // Successful, non-retryable response: return immediately - if (response && !retryStatusCodes.includes(response.status)) { + if (!retryStatusCodes.includes(response.status)) { return response; } - // Out of attempts: surface the response or rethrow the captured error + // No more attempts left — return the last (retryable) response. if (attempt++ >= maxRetries) { - if (response) return response; - throw error; + return response; } // Determine delay from Retry-After header if present, otherwise exponential backoff - const retryAfter = response?.headers.get('Retry-After'); + const retryAfter = response.headers.get('Retry-After'); const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 - : Math.min(100 * Math.pow(2, attempt - 1), 10000); // Exponential backoff, cap at 10 seconds + : backoffDelay(attempt); // Release the underlying connection so it can be reused, since we're // about to discard this response in favor of a retry. - await response?.body?.cancel().catch(() => {}); + await response.body?.cancel().catch(() => {}); await sleep(delay, abortSignal); }