Skip to content

Commit e404f39

Browse files
committed
Handled aborts during retry backoff
1 parent 2c40873 commit e404f39

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ export function createClient<
276276
effectiveRetries,
277277
effectiveRetryDelay,
278278
shouldRetryWithHook,
279-
request
279+
request,
280+
combinedSignal
280281
)
281282
if (effectiveHooks.transformResponse) {
282283
res = await effectiveHooks.transformResponse(res, request)

src/retry.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,40 @@ export const defaultDelay: RetryDelay = (ctx) => {
1313
return 2 ** ctx.attempt * 200 + Math.random() * 100
1414
}
1515

16+
function waitForRetryDelay(ms: number, signal?: AbortSignal): Promise<void> {
17+
if (ms <= 0) return Promise.resolve()
18+
return new Promise((resolve) => {
19+
if (!signal) {
20+
setTimeout(resolve, ms)
21+
return
22+
}
23+
24+
if (signal.aborted) {
25+
resolve()
26+
return
27+
}
28+
29+
const onAbort = () => {
30+
clearTimeout(timer)
31+
resolve()
32+
}
33+
34+
const timer = setTimeout(() => {
35+
signal.removeEventListener('abort', onAbort)
36+
resolve()
37+
}, ms)
38+
39+
signal.addEventListener('abort', onAbort, { once: true })
40+
})
41+
}
42+
1643
export async function retry(
1744
fn: () => Promise<Response>,
1845
retries: number,
1946
delay: RetryDelay,
2047
shouldRetry: (ctx: RetryContext) => boolean = () => true,
21-
request: Request
48+
request: Request,
49+
signal?: AbortSignal
2250
): Promise<Response> {
2351
let lastErr: unknown
2452
let lastRes: Response | undefined
@@ -36,7 +64,7 @@ export async function retry(
3664
ctx.error = undefined
3765
if (i < retries && shouldRetry(ctx)) {
3866
const wait = typeof delay === 'function' ? delay(ctx) : delay
39-
await new Promise((r) => setTimeout(r, wait))
67+
await waitForRetryDelay(wait, signal)
4068
continue
4169
}
4270
return lastRes
@@ -45,7 +73,7 @@ export async function retry(
4573
ctx.error = err
4674
if (i === retries || !shouldRetry(ctx)) throw err
4775
const wait = typeof delay === 'function' ? delay(ctx) : delay
48-
await new Promise((r) => setTimeout(r, wait))
76+
await waitForRetryDelay(wait, signal)
4977
}
5078
}
5179
throw lastErr

test/core/client.error.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,30 @@ describe('Advanced/Edge Cases: Custom Errors', () => {
293293
(err instanceof AbortError &&
294294
err.message === 'Request was aborted by user' &&
295295
err.cause === undefined) ||
296-
(err instanceof TimeoutError && err.cause instanceof DOMException)
296+
err instanceof TimeoutError
297297
)
298298
expect(abortFired).toBe(true)
299299
}, 1000)
300300

301+
it('AbortError: thrown when user aborts during retry backoff delay', async () => {
302+
const fetchSpy = vi.fn().mockRejectedValue(new Error('first failure'))
303+
304+
global.fetch = fetchSpy
305+
const controller = new AbortController()
306+
const f = createClient({ retries: 1, retryDelay: 10_000 })
307+
308+
const promise = f('https://example.com', { signal: controller.signal })
309+
310+
setTimeout(() => controller.abort(), 5)
311+
312+
await expect(promise).rejects.toSatisfy(
313+
(err) =>
314+
err instanceof AbortError &&
315+
err.message === 'Request was aborted by user'
316+
)
317+
expect(fetchSpy).toHaveBeenCalledTimes(1)
318+
})
319+
301320
it('NetworkError: thrown for different network error messages', async () => {
302321
const nativeErr = new TypeError('NetworkError: lost connection')
303322
global.fetch = vi.fn().mockRejectedValue(nativeErr)

0 commit comments

Comments
 (0)