diff --git a/src/fetch.ts b/src/fetch.ts index 10c91583..d0a407ef 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -54,12 +54,18 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { retries = isPayloadMethod(context.options.method) ? 0 : 1; } - const responseCode = (context.response && context.response.status) || 500; + // When there is no response (network / connection error), bypass the + // status code check and always retry. Previously the code fell back to + // 500, which caused requests to silently stop being retried whenever + // callers supplied a custom retryStatusCodes array that excluded 500. + const hasResponse = !!(context.response && context.response.status); + const responseCode = hasResponse ? context.response!.status : undefined; if ( retries > 0 && - (Array.isArray(context.options.retryStatusCodes) - ? context.options.retryStatusCodes.includes(responseCode) - : retryStatusCodes.has(responseCode)) + (!hasResponse || + (Array.isArray(context.options.retryStatusCodes) + ? context.options.retryStatusCodes.includes(responseCode!) + : retryStatusCodes.has(responseCode!))) ) { const retryDelay = typeof context.options.retryDelay === "function" diff --git a/test/index.test.ts b/test/index.test.ts index 5ac20b07..4f2f55de 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -309,6 +309,33 @@ describe("ofetch", () => { await expect(abortHandle()).rejects.toThrow(/aborted/); }); + it("network errors are retried even when retryStatusCodes excludes 500", async () => { + // Simulate a network-level failure (no HTTP response at all). + // Previously, ofetch fell back to status code 500 when context.response + // was undefined. A custom retryStatusCodes array that excluded 500 would + // therefore also suppress retries for connection errors. + let callCount = 0; + const networkError = new TypeError("Failed to fetch"); + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => { + callCount++; + throw networkError; + }; + try { + await $fetch("https://example.com", { + retry: 2, + // 500 is intentionally absent; only proxy-related codes are listed. + retryStatusCodes: [502, 503, 504], + }); + } catch { + // expected to throw after exhausting retries + } finally { + globalThis.fetch = originalFetch; + } + // Initial attempt + 2 retries = 3 calls total. + expect(callCount).toBe(3); + }); + it("passing request obj should return request obj in error", async () => { const error = await $fetch(getURL("/403"), { method: "post" }).catch( (error: any) => error