fix(utils): normalized aborts during retry backoff
Route both fetchWithRetry backoff paths through one abort-normalizing wait helper and cover response and network retry cancellation. Fixes #8450
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `fetchWithRetry()` aborts during retry backoff to preserve the documented `"Request was aborted"` error contract ([#8450](https://github.com/can1357/oh-my-pi/issues/8450)).
|
||||
|
||||
## [17.3.0] - 2026-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -220,7 +220,7 @@ export async function fetchWithRetry(
|
||||
if (signal?.aborted) throw new Error("Request was aborted");
|
||||
const wrapped = wrapNetworkError(error);
|
||||
if (attempt + 1 >= maxAttempts) throw wrapped;
|
||||
await scheduler.wait(resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), { signal });
|
||||
await waitForRetry(resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), signal);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function fetchWithRetry(
|
||||
if (hint !== undefined && hint > maxDelayMs) return response;
|
||||
|
||||
const delayMs = Math.min(hint ?? resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), maxDelayMs);
|
||||
await scheduler.wait(delayMs, { signal });
|
||||
await waitForRetry(delayMs, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,15 @@ function mergeInit(base: RequestInit, overlay: RequestInit, timeout: number | fa
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function waitForRetry(delayMs: number, signal: AbortSignal | undefined): Promise<void> {
|
||||
try {
|
||||
await scheduler.wait(delayMs, { signal });
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw new Error("Request was aborted");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function wrapNetworkError(error: unknown): Error {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "AbortError" || error.message === "Request was aborted") {
|
||||
|
||||
@@ -78,6 +78,36 @@ describe("fetchWithRetry", () => {
|
||||
expect(await response.text()).toBe("slow down");
|
||||
expect(attempt).toBe(1);
|
||||
});
|
||||
|
||||
it("normalizes aborts during response backoff", async () => {
|
||||
const request = fetchWithRetry("https://example.invalid/response-backoff", {
|
||||
fetch: async () => new Response("retry", { status: 503 }),
|
||||
signal: AbortSignal.timeout(10),
|
||||
defaultDelayMs: 1_000,
|
||||
maxAttempts: 2,
|
||||
});
|
||||
|
||||
await expect(request).rejects.toMatchObject({
|
||||
name: "Error",
|
||||
message: "Request was aborted",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes aborts during network-error backoff", async () => {
|
||||
const request = fetchWithRetry("https://example.invalid/network-backoff", {
|
||||
fetch: async () => {
|
||||
throw new TypeError("connection reset");
|
||||
},
|
||||
signal: AbortSignal.timeout(10),
|
||||
defaultDelayMs: 1_000,
|
||||
maxAttempts: 2,
|
||||
});
|
||||
|
||||
await expect(request).rejects.toMatchObject({
|
||||
name: "Error",
|
||||
message: "Request was aborted",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractRetryHint", () => {
|
||||
|
||||
Reference in New Issue
Block a user