diff --git a/src/lib/ae_api/api_delete_object.ts b/src/lib/ae_api/api_delete_object.ts index 7d37ba98..4a6ef9e8 100644 --- a/src/lib/ae_api/api_delete_object.ts +++ b/src/lib/ae_api/api_delete_object.ts @@ -97,9 +97,15 @@ export const delete_object = async function delete_object({ } for (let attempt = 1; attempt <= retry_count; attempt++) { + // Keep timeout handle at attempt scope so catch can always clear it. + let timeoutId: ReturnType | null = null; try { const controller = new AbortController(); - const timeoutId = setTimeout(() => { + // AbortError alone is ambiguous. Track helper-timeout aborts so + // caller/navigation aborts can still fail fast with no retry. + let did_timeout_abort = false; + timeoutId = setTimeout(() => { + did_timeout_abort = true; console.error( `API DELETE request timed out after ${timeout}ms.` ); @@ -120,12 +126,48 @@ export const delete_object = async function delete_object({ url.toString(), fetchOptions ).catch(function (error: any) { + if ( + error?.name === 'AbortError' || + error?.name === 'TypeError' || + error?.message?.includes('aborted') + ) { + if (log_lvl > 1) { + console.log( + 'API DELETE: Request aborted or browser-terminated.', + error + ); + } + return error; + } + console.log( 'API DELETE 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') { + if (did_timeout_abort) { + throw new Error( + `Timeout abort (attempt ${attempt}/${retry_count}) after ${timeout}ms` + ); + } + return false; + } + + throw new Error( + `Network error (attempt ${attempt}): ${response.message}` + ); + } if (!response) { throw new Error( @@ -151,7 +193,13 @@ export const delete_object = async function delete_object({ errorBody ); - if (response.status >= 400 && response.status < 404) { + // Fail fast on client/auth/validation failures. + if ( + response.status === 400 || + response.status === 401 || + response.status === 403 || + response.status === 422 + ) { return false; } @@ -174,6 +222,8 @@ export const delete_object = async function delete_object({ ? json.data : json; } catch (error) { + // Ensure per-attempt timeout is always cleared on failure. + if (timeoutId) clearTimeout(timeoutId); console.error(`API DELETE error on attempt ${attempt}:`, error); if (attempt === retry_count) { @@ -181,9 +231,12 @@ export const delete_object = async function delete_object({ return false; } - if (log_lvl) { - console.log(`Retrying... (${attempt}/${retry_count})`); - } + // Backoff before retrying. Caps at 8s to match GET/POST/PATCH policy. + const delay_ms = Math.min(2000 * attempt, 8000); + console.log( + `API DELETE: Retrying in ${delay_ms}ms... (attempt ${attempt}/${retry_count})` + ); + await new Promise((resolve) => setTimeout(resolve, delay_ms)); } } };