Skip to content

Commit a8d70ab

Browse files
committed
feat: add retryIf option (#503)
1 parent d61b2fc commit a8d70ab

File tree

3 files changed

+106
-6
lines changed

3 files changed

+106
-6
lines changed

src/fetch.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,19 @@ export function createFetch(globalOptions: CreateFetchOptions = {}): $Fetch {
5555
}
5656

5757
const responseCode = (context.response && context.response.status) || 500;
58-
if (
59-
retries > 0 &&
60-
(Array.isArray(context.options.retryStatusCodes)
61-
? context.options.retryStatusCodes.includes(responseCode)
62-
: retryStatusCodes.has(responseCode))
63-
) {
58+
59+
const isRetryableStatus = Array.isArray(context.options.retryStatusCodes)
60+
? context.options.retryStatusCodes.includes(responseCode)
61+
: retryStatusCodes.has(responseCode);
62+
63+
const isConditionalRetry =
64+
typeof context.options.retryIf === "function"
65+
? await context.options.retryIf(context)
66+
: false;
67+
68+
const shouldRetry = isRetryableStatus || isConditionalRetry;
69+
70+
if (retries > 0 && shouldRetry) {
6471
const retryDelay =
6572
typeof context.options.retryDelay === "function"
6673
? context.options.retryDelay(context)

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export interface FetchOptions<R extends ResponseType = ResponseType, T = any>
6868

6969
/** Default is [408, 409, 425, 429, 500, 502, 503, 504] */
7070
retryStatusCodes?: number[];
71+
72+
/**
73+
* Condition to retry the request.
74+
* @default false
75+
*/
76+
retryIf?: (context: FetchContext<T, R>) => boolean | Promise<boolean>;
7177
}
7278

7379
export interface ResolvedFetchOptions<

test/index.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,93 @@ describe("ofetch", () => {
344344
});
345345
});
346346

347+
it("retries when retryIf (sync) returns true", async () => {
348+
let called = 0;
349+
await $fetch(getURL("408"), {
350+
retry: 2,
351+
retryIf: () => {
352+
called++;
353+
return true;
354+
},
355+
}).catch(() => "failed");
356+
expect(called).to.equal(3);
357+
});
358+
359+
it("retries when retryIf (async) returns true", async () => {
360+
let called = 0;
361+
await $fetch(getURL("408"), {
362+
retry: 2,
363+
retryIf: async () => {
364+
called++;
365+
return true;
366+
},
367+
}).catch(() => "failed");
368+
expect(called).to.equal(3);
369+
});
370+
371+
it("does not retry when retryIf returns false and status does not match", async () => {
372+
let called = 0;
373+
await $fetch(getURL("404"), {
374+
retry: 2,
375+
retryIf: () => {
376+
called++;
377+
return false;
378+
},
379+
}).catch(() => "failed");
380+
expect(called).to.equal(1);
381+
});
382+
383+
it("retries on matching status even if retryIf returns false", async () => {
384+
let called = 0;
385+
await $fetch(getURL("408"), {
386+
retry: 2,
387+
retryIf: () => {
388+
called++;
389+
return false;
390+
},
391+
}).catch(() => "failed");
392+
expect(called).to.equal(3);
393+
});
394+
395+
it("throws if retryIf throws (consumer error) propagates error to consumer", async () => {
396+
let called = 0;
397+
await expect(
398+
$fetch(getURL("408"), {
399+
retry: 2,
400+
retryIf: () => {
401+
called++;
402+
throw new Error("bad predicate");
403+
},
404+
})
405+
).rejects.toThrow("bad predicate");
406+
expect(called).to.equal(1);
407+
});
408+
409+
it("retries when both retryStatusCodes and retryIf allow", async () => {
410+
let called = 0;
411+
await $fetch(getURL("408"), {
412+
retry: 2,
413+
retryStatusCodes: [408],
414+
retryIf: () => {
415+
called++;
416+
return true;
417+
},
418+
}).catch(() => "failed");
419+
expect(called).to.equal(3);
420+
});
421+
422+
it("retries when only retryStatusCodes match and retryIf is absent", async () => {
423+
let called = 0;
424+
await $fetch(getURL("408"), {
425+
retry: 2,
426+
retryStatusCodes: [408],
427+
onResponseError: () => {
428+
called++;
429+
},
430+
}).catch(() => "failed");
431+
expect(called).to.equal(3);
432+
});
433+
347434
it("deep merges defaultOptions", async () => {
348435
const _customFetch = $fetch.create({
349436
query: {

0 commit comments

Comments
 (0)