From eda4ae3f764f19369db57e4d0202b8aa64c9ea5e Mon Sep 17 00:00:00 2001 From: Enkot Date: Mon, 5 Feb 2024 22:15:56 +0200 Subject: [PATCH 1/5] feat: retry callback --- src/fetch.ts | 14 ++++++++------ src/types.ts | 2 ++ test/index.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index c0021c08..7503f403 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -56,12 +56,14 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { } const responseCode = (context.response && context.response.status) || 500; - if ( - retries > 0 && - (Array.isArray(context.options.retryStatusCodes) - ? context.options.retryStatusCodes.includes(responseCode) - : retryStatusCodes.has(responseCode)) - ) { + const hasStatusCode = Array.isArray(context.options.retryStatusCodes) + ? context.options.retryStatusCodes.includes(responseCode) + : retryStatusCodes.has(responseCode); + const shouldRetry = context.options.retryCb + ? await context.options.retryCb(context) + : hasStatusCode; + + if (retries > 0 && shouldRetry) { const retryDelay = context.options.retryDelay || 0; if (retryDelay > 0) { await new Promise((resolve) => setTimeout(resolve, retryDelay)); diff --git a/src/types.ts b/src/types.ts index d4f2e11c..b4560a41 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,8 @@ export interface FetchOptions /** Default is [408, 409, 425, 429, 500, 502, 503, 504] */ retryStatusCodes?: number[]; + retryCb?: (context: FetchContext) => Promise | boolean; + onRequest?(context: FetchContext): Promise | void; onRequestError?( context: FetchContext & { error: Error } diff --git a/test/index.test.ts b/test/index.test.ts index de25338f..6f22f5b5 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -287,6 +287,22 @@ describe("ofetch", () => { expect(race).to.equal("fast"); }); + it("retry callback", async () => { + const slow = $fetch(getURL("408"), { + retry: 2, + retryDelay: 100, + retryCb: () => false, + }).catch(() => "slow"); + const fast = $fetch(getURL("408"), { + retry: 2, + retryDelay: 1, + retryCb: () => true, + }).catch(() => "fast"); + + const race = await Promise.race([slow, fast]); + expect(race).to.equal("slow"); + }); + it("abort with retry", () => { const controller = new AbortController(); async function abortHandle() { From bdd28b4ff786553ef00345b54aed5771234731b3 Mon Sep 17 00:00:00 2001 From: Enkot Date: Mon, 5 Feb 2024 22:20:53 +0200 Subject: [PATCH 2/5] feat: simpler tests --- test/index.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index 6f22f5b5..d47b3c3d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -290,12 +290,10 @@ describe("ofetch", () => { it("retry callback", async () => { const slow = $fetch(getURL("408"), { retry: 2, - retryDelay: 100, retryCb: () => false, }).catch(() => "slow"); const fast = $fetch(getURL("408"), { retry: 2, - retryDelay: 1, retryCb: () => true, }).catch(() => "fast"); From 1ffd12dbcaf1bb604d7d86c0f40a4462dd135b56 Mon Sep 17 00:00:00 2001 From: Enkot Date: Mon, 5 Feb 2024 22:30:48 +0200 Subject: [PATCH 3/5] fix: docs --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c7401052..d97f4589 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,13 @@ The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DE The default for `retryDelay` is `0` ms. +You can also use `retryCb` function to retry on some condition. It takes a fetch context object as an argument. + ```ts await ofetch("http://google.com/404", { retry: 3, retryDelay: 500, // ms + retryCb: (ctx) => ctx.error.code === '007' }); ``` From daed2019bf2d5d754cc1c95feb41b0113c2b9633 Mon Sep 17 00:00:00 2001 From: Enkot Date: Fri, 8 Mar 2024 00:37:12 +0200 Subject: [PATCH 4/5] fix: refactored implementation --- src/fetch.ts | 31 ++++++++++++++++++++++--------- src/types.ts | 7 ++++--- test/index.test.ts | 15 ++++----------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/fetch.ts b/src/fetch.ts index 7503f403..f18a5e2c 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -37,7 +37,9 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { AbortController = globalThis.AbortController, } = globalOptions; - async function onError(context: FetchContext): Promise> { + async function getRetryResponse( + context: FetchContext + ): Promise | void> { // Is Abort // If it is an active abort, it will not retry automatically. // https://developer.mozilla.org/en-US/docs/Web/API/DOMException#error_names @@ -59,23 +61,32 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { const hasStatusCode = Array.isArray(context.options.retryStatusCodes) ? context.options.retryStatusCodes.includes(responseCode) : retryStatusCodes.has(responseCode); - const shouldRetry = context.options.retryCb - ? await context.options.retryCb(context) - : hasStatusCode; + // @ts-expect-error value for internal use + const { retry, retryDelay = 0, _retryCount = 1 } = context.options; + const shouldRetry = + typeof retry === "function" ? await retry(context, _retryCount) : false; - if (retries > 0 && shouldRetry) { - const retryDelay = context.options.retryDelay || 0; + if ((retries > 0 && hasStatusCode) || shouldRetry) { if (retryDelay > 0) { await new Promise((resolve) => setTimeout(resolve, retryDelay)); } // Timeout return $fetchRaw(context.request, { ...context.options, - retry: retries - 1, - timeout: context.options.timeout, + retry: typeof retry === "function" ? retry : retries - 1, + // @ts-expect-error value for internal use + _retryCount: _retryCount + 1, }); } } + } + + async function onError(context: FetchContext): Promise> { + const retryResponse = await getRetryResponse(context); + + if (retryResponse) { + return retryResponse; + } // Throw normalized error const error = createFetchError(context); @@ -215,7 +226,9 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch { return await onError(context); } - return context.response; + const retryResponse = await getRetryResponse(context); + + return retryResponse || context.response; }; const $fetch = async function $fetch(request, options) { diff --git a/src/types.ts b/src/types.ts index b4560a41..7697a6f7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,14 +51,15 @@ export interface FetchOptions /** timeout in milliseconds */ timeout?: number; - retry?: number | false; + retry?: + | number + | false + | ((context: FetchContext, count: number) => Promise | boolean); /** Delay between retries in milliseconds. */ retryDelay?: number; /** Default is [408, 409, 425, 429, 500, 502, 503, 504] */ retryStatusCodes?: number[]; - retryCb?: (context: FetchContext) => Promise | boolean; - onRequest?(context: FetchContext): Promise | void; onRequestError?( context: FetchContext & { error: Error } diff --git a/test/index.test.ts b/test/index.test.ts index d47b3c3d..86c13005 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -9,7 +9,7 @@ import { readRawBody, toNodeListener, } from "h3"; -import { describe, beforeAll, afterAll, it, expect } from "vitest"; +import { describe, beforeAll, afterAll, it, expect, vi } from "vitest"; import { Headers, FormData, Blob } from "node-fetch-native"; import { nodeMajorVersion } from "std-env"; import { $fetch } from "../src/node"; @@ -288,17 +288,10 @@ describe("ofetch", () => { }); it("retry callback", async () => { - const slow = $fetch(getURL("408"), { - retry: 2, - retryCb: () => false, - }).catch(() => "slow"); - const fast = $fetch(getURL("408"), { - retry: 2, - retryCb: () => true, - }).catch(() => "fast"); + const retry = vi.fn().mockImplementation((_, count) => count <= 3); + await $fetch(getURL("ok"), { retry }); - const race = await Promise.race([slow, fast]); - expect(race).to.equal("slow"); + expect(retry).toHaveBeenCalledTimes(4); }); it("abort with retry", () => { From 7fe698b0a53e4bbe8a2b296e2e354200575cc437 Mon Sep 17 00:00:00 2001 From: Enkot Date: Fri, 8 Mar 2024 00:55:02 +0200 Subject: [PATCH 5/5] fix: docs --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d97f4589..bb880cc7 100644 --- a/README.md +++ b/README.md @@ -129,13 +129,20 @@ The default for `retry` is `1` retry, except for `POST`, `PUT`, `PATCH`, and `DE The default for `retryDelay` is `0` ms. -You can also use `retryCb` function to retry on some condition. It takes a fetch context object as an argument. - ```ts await ofetch("http://google.com/404", { retry: 3, retryDelay: 500, // ms - retryCb: (ctx) => ctx.error.code === '007' +}); +``` + +You can also pass a callback as a `retry` option. It takes a fetch context object and the count of retries and returns a boolean. + +```ts +await $fetch("/api", { + retry: (ctx, count) => { + return count <= 3 && ctx.error?.code === "007"; + }, }); ```