diff --git a/src/util/retry-fetch.spec.ts b/src/util/retry-fetch.spec.ts new file mode 100644 index 0000000..53d9586 --- /dev/null +++ b/src/util/retry-fetch.spec.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, beforeEach, afterEach, mock } from "bun:test"; +import { retryFetch } from "./retry-fetch"; +import { AbortError } from "p-retry"; + +// Mock the global fetch +const originalFetch = global.fetch; +let mockFetch: ReturnType; + +describe("retryFetch", () => { + beforeEach(() => { + // Reset mock before each test + mockFetch = mock(() => {}); + global.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockReset(); + }); + + it("should not retry on 401 unauthorized", async () => { + // Mock fetch to return 401 + mockFetch.mockImplementation(() => + Promise.resolve({ + status: 401, + json: () => Promise.resolve({ message: "Unauthorized" }), + }) + ); + + // Attempt the fetch + await expect(retryFetch("/test", {})).rejects.toThrow(); + + // Verify fetch was only called once + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it("should retry on 500 GET server error", async () => { + // Mock fetch to fail twice with 500, then succeed + mockFetch + .mockImplementationOnce(() => + Promise.resolve({ + status: 500, + json: () => Promise.resolve({ message: "Server Error" }), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + status: 500, + json: () => Promise.resolve({ message: "Server Error" }), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({ message: "Success" }), + }) + ); + + // Attempt the fetch + await retryFetch("/test", {}); + + // Verify fetch was called multiple times + expect(mockFetch).toHaveBeenCalledTimes(3); + }); + + it("should retry on 500 POST server error", async () => { + // Mock fetch to fail twice with 500, then succeed + mockFetch + .mockImplementationOnce(() => + Promise.resolve({ + status: 500, + json: () => Promise.resolve({ message: "Server Error" }), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + status: 500, + json: () => Promise.resolve({ message: "Server Error" }), + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({ message: "Success" }), + }) + ); + + // Attempt the fetch + await retryFetch("/test", { + method: "POST", + }); + + // Verify fetch was called multiple times + expect(mockFetch).toHaveBeenCalledTimes(3); + }); + + it("should succeed immediately on 200", async () => { + // Mock fetch to return 200 + mockFetch.mockImplementation(() => + Promise.resolve({ + status: 200, + json: () => Promise.resolve({ message: "Success" }), + }) + ); + + // Attempt the fetch + await retryFetch("/test", {}); + + // Verify fetch was only called once + expect(mockFetch.mock.calls.length).toBe(1); + }); +}); diff --git a/src/util/retry-fetch.ts b/src/util/retry-fetch.ts index b6ba861..80aa72c 100644 --- a/src/util/retry-fetch.ts +++ b/src/util/retry-fetch.ts @@ -38,7 +38,15 @@ async function wrappedFetch(...args: Parameters) { } // Source: https://github.com/sindresorhus/got/blob/main/documentation/7-retry.md -const retryMethods = ["GET", "PUT", "HEAD", "DELETE", "OPTIONS", "TRACE"]; +const retryMethods = [ + "GET", + "PUT", + "HEAD", + "DELETE", + "OPTIONS", + "TRACE", + "POST", // This is not in the default, but I think it makes sense for us +]; const retryStatusCodes = [408, 413, 429, 500, 502, 503, 504, 521, 522, 524]; const retryErrorCodes = [ "ETIMEDOUT",