api: harden patch retry classification and backoff
This commit is contained in:
@@ -153,9 +153,15 @@ export const patch_object = async function patch_object({
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
||||
// Keep timeout handle at attempt scope so catch can always clear it.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
// AbortError alone is ambiguous. Track whether the helper timeout
|
||||
// fired so we can retry timeout-aborts but fail fast on caller abort.
|
||||
let did_timeout_abort = false;
|
||||
timeoutId = setTimeout(() => {
|
||||
did_timeout_abort = true;
|
||||
console.error(
|
||||
`API PATCH request timed out after ${timeout}ms.`
|
||||
);
|
||||
@@ -173,12 +179,52 @@ export const patch_object = async function patch_object({
|
||||
url.toString(),
|
||||
fetchOptions
|
||||
).catch(function (error: any) {
|
||||
// Keep noisy abort/network conditions out of high-level logs.
|
||||
if (
|
||||
error?.name === 'AbortError' ||
|
||||
error?.name === 'TypeError' ||
|
||||
error?.message?.includes('aborted')
|
||||
) {
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'API PATCH: Request aborted or browser-terminated.',
|
||||
error
|
||||
);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'API PATCH Object *fetch* request was aborted or failed in an unexpected way.',
|
||||
error
|
||||
);
|
||||
return error;
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
|
||||
// Error object was returned from fetch catch block; decide retry class.
|
||||
if (
|
||||
response instanceof Error ||
|
||||
(response &&
|
||||
(response.name === 'AbortError' ||
|
||||
response.name === 'TypeError'))
|
||||
) {
|
||||
if (response.name === 'AbortError') {
|
||||
// Retry only helper-timeout aborts. Caller/navigation aborts
|
||||
// should fail fast to avoid duplicate mutation side-effects.
|
||||
if (did_timeout_abort) {
|
||||
throw new Error(
|
||||
`Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transient browser/network failure class.
|
||||
throw new Error(
|
||||
`Network error (attempt ${attempt}): ${response.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error(
|
||||
@@ -292,6 +338,8 @@ export const patch_object = async function patch_object({
|
||||
? json.data
|
||||
: json;
|
||||
} catch (error) {
|
||||
// Ensure per-attempt timeout is always cleared on failure.
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
console.error(`API PATCH error on attempt ${attempt}:`, error);
|
||||
|
||||
if (attempt === retry_count) {
|
||||
@@ -299,9 +347,12 @@ export const patch_object = async function patch_object({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (log_lvl) {
|
||||
console.log(`Retrying... (${attempt}/${retry_count})`);
|
||||
}
|
||||
// Backoff before retrying. Caps at 8s to match GET/POST policy.
|
||||
const delay_ms = Math.min(2000 * attempt, 8000);
|
||||
console.log(
|
||||
`API PATCH: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})`
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, delay_ms));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user