diff --git a/__tests__/download/manifest.test.ts b/__tests__/download/manifest.test.ts index b269335..61d4a48 100644 --- a/__tests__/download/manifest.test.ts +++ b/__tests__/download/manifest.test.ts @@ -1,4 +1,11 @@ -import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from "@jest/globals"; // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. const mockFetch = jest.fn(); @@ -13,6 +20,7 @@ jest.unstable_mockModule("../../src/utils/fetch", () => ({ })); const { + MANIFEST_FETCH_ATTEMPTS, clearManifestCache, fetchManifest, getAllVersions, @@ -73,6 +81,10 @@ describe("manifest", () => { mockFetch.mockReset(); }); + afterEach(() => { + jest.useRealTimers(); + }); + describe("fetchManifest", () => { it("fetches and parses manifest data", async () => { mockFetch.mockResolvedValue( @@ -86,6 +98,34 @@ describe("manifest", () => { expect(versions[1]?.version).toBe("0.9.25"); }); + it("retries network failures", async () => { + jest.useFakeTimers(); + mockFetch + .mockRejectedValueOnce(new Error("request timed out")) + .mockResolvedValueOnce( + createMockResponse(true, 200, "OK", sampleManifestResponse), + ); + + const result = fetchManifest(); + await jest.runAllTimersAsync(); + + await expect(result).resolves.toHaveLength(2); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it("stops after the configured number of network failures", async () => { + jest.useFakeTimers(); + mockFetch.mockRejectedValue(new Error("request timed out")); + + const result = expect(fetchManifest()).rejects.toThrow( + "request timed out", + ); + await jest.runAllTimersAsync(); + + await result; + expect(mockFetch).toHaveBeenCalledTimes(MANIFEST_FETCH_ATTEMPTS); + }); + it("throws on a failed fetch", async () => { mockFetch.mockResolvedValue( createMockResponse(false, 500, "Internal Server Error", ""), diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index cd1df7b..34fc0c1 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -99724,6 +99724,7 @@ function formatVariants(entries) { // src/download/manifest.ts var cachedManifestData = /* @__PURE__ */ new Map(); +var MANIFEST_FETCH_ATTEMPTS = 3; async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) { const cachedManifest = cachedManifestData.get(manifestUrl); if (cachedManifest?.complete === true) { @@ -99802,8 +99803,24 @@ async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MA }; } async function fetchManifestResponse(manifestUrl) { - info2(`Fetching manifest data from ${manifestUrl} ...`); - const response = await fetch(manifestUrl, {}); + let response; + for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { + info2(`Fetching manifest data from ${manifestUrl} ...`); + try { + response = await fetch(manifestUrl, {}); + break; + } catch (error2) { + if (attempt >= MANIFEST_FETCH_ATTEMPTS) { + throw error2; + } + const delayMs = 1e3 * 2 ** (attempt - 1); + info2(`Manifest fetch failed; retrying in ${delayMs}ms ...`); + await new Promise((resolve3) => setTimeout(resolve3, delayMs)); + } + } + if (response === void 0) { + throw new Error("Manifest fetch attempts exhausted."); + } if (!response.ok) { throw new Error( `Failed to fetch manifest data: ${response.status} ${response.statusText}` diff --git a/dist/update-known-checksums/index.cjs b/dist/update-known-checksums/index.cjs index 1deaa1a..6fde91a 100644 --- a/dist/update-known-checksums/index.cjs +++ b/dist/update-known-checksums/index.cjs @@ -52390,6 +52390,7 @@ function info2(msg) { // src/download/manifest.ts var cachedManifestData = /* @__PURE__ */ new Map(); +var MANIFEST_FETCH_ATTEMPTS = 3; async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) { const cachedManifest = cachedManifestData.get(manifestUrl); if (cachedManifest?.complete === true) { @@ -52430,8 +52431,24 @@ async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) { return latestVersion; } async function fetchManifestResponse(manifestUrl) { - info2(`Fetching manifest data from ${manifestUrl} ...`); - const response = await fetch(manifestUrl, {}); + let response; + for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { + info2(`Fetching manifest data from ${manifestUrl} ...`); + try { + response = await fetch(manifestUrl, {}); + break; + } catch (error2) { + if (attempt >= MANIFEST_FETCH_ATTEMPTS) { + throw error2; + } + const delayMs = 1e3 * 2 ** (attempt - 1); + info2(`Manifest fetch failed; retrying in ${delayMs}ms ...`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + if (response === void 0) { + throw new Error("Manifest fetch attempts exhausted."); + } if (!response.ok) { throw new Error( `Failed to fetch manifest data: ${response.status} ${response.statusText}` diff --git a/src/download/manifest.ts b/src/download/manifest.ts index b26bf82..91763e7 100644 --- a/src/download/manifest.ts +++ b/src/download/manifest.ts @@ -31,6 +31,7 @@ interface CachedManifest { } const cachedManifestData = new Map(); +export const MANIFEST_FETCH_ATTEMPTS = 3; export async function fetchManifest( manifestUrl: string = VERSIONS_MANIFEST_URL, @@ -166,8 +167,26 @@ export function clearManifestCache(manifestUrl?: string): void { } async function fetchManifestResponse(manifestUrl: string) { - log.info(`Fetching manifest data from ${manifestUrl} ...`); - const response = await fetch(manifestUrl, {}); + let response: Awaited> | undefined; + for (let attempt = 1; attempt <= MANIFEST_FETCH_ATTEMPTS; attempt++) { + log.info(`Fetching manifest data from ${manifestUrl} ...`); + try { + response = await fetch(manifestUrl, {}); + break; + } catch (error) { + if (attempt >= MANIFEST_FETCH_ATTEMPTS) { + throw error; + } + const delayMs = 1_000 * 2 ** (attempt - 1); + log.info(`Manifest fetch failed; retrying in ${delayMs}ms ...`); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + + if (response === undefined) { + throw new Error("Manifest fetch attempts exhausted."); + } + if (!response.ok) { throw new Error( `Failed to fetch manifest data: ${response.status} ${response.statusText}`,