UNPKG

@joinmeow/cognito-passwordless-auth

Version:

Passwordless authentication with Amazon Cognito: FIDO2 (WebAuthn, support for Passkeys)

100 lines (99 loc) 4.49 kB
/** * Return a fetch function that will retry on network errors, HTTP 5xx, * and specific retryable 400 errors (TooManyRequestsException, * LimitExceededException, CodeDeliveryFailureException), up to `maxRetries` * times with exponential backoff. * * Uses `Response.clone()` to inspect 400 error bodies without consuming the * original response body. */ export function createFetchWithRetry(fetchFn, debugFn, maxRetries = 3, baseDelayMs = 1000) { const retryableErrors = new Set([ "TooManyRequestsException", "LimitExceededException", "CodeDeliveryFailureException", ]); return async (input, init) => { // Helper to wait with abort support const wait = (ms) => { if (init?.signal?.aborted) { return Promise.reject(new DOMException("Aborted", "AbortError")); } return new Promise((resolve, reject) => { const id = setTimeout(resolve, ms); init?.signal?.addEventListener("abort", () => { clearTimeout(id); reject(new DOMException("Aborted", "AbortError")); }, { once: true }); }); }; for (let attempt = 1; attempt <= maxRetries; attempt++) { // Abort before starting the attempt if (init?.signal?.aborted) { throw new DOMException("Aborted", "AbortError"); } try { const res = await fetchFn(input, init); if (res.ok) { return res; } if (res.status === 400) { // Retry on specific 400 errors by cloning the response const nativeRes = res; if (typeof nativeRes.clone === "function") { let retryErrorType; try { const cloned = nativeRes.clone(); const errObj = (await cloned.json()); const errorType = errObj.__type; if (errorType && retryableErrors.has(errorType)) { retryErrorType = errorType; // mark for retry after we leave the try/catch } } catch (parseError) { debugFn?.("Failed to parse error response for retryable type:", parseError); } // Evaluate retry after JSON parse to avoid the thrown error being // swallowed by the try/catch above. if (retryErrorType) { if (attempt === maxRetries) { // On the last attempt, surface the response (like we do for 5xx) return res; } // Throw to trigger retry logic outside of the nested try/catch throw new Error(retryErrorType); } } return res; } // Retry on server errors (5xx), status 0 (network error), or undefined status const status = res.status; if (status === undefined || status === 0 || status >= 500) { if (attempt === maxRetries) { // Final attempt: return response so caller can parse error body return res; } // Retry on transient server or network error throw new Error(`ServerError:${status}`); } return res; } catch (error) { if (init?.signal?.aborted || (error instanceof Error && error.name === "AbortError")) { throw error; } if (attempt === maxRetries) { throw error; } debugFn?.(`fetchWithRetry attempt ${attempt}/${maxRetries} failed:`, error); // Exponential backoff for all errors: 1s, 2s, 4s const backoff = baseDelayMs * Math.pow(2, attempt - 1); debugFn?.(`Retrying in ${backoff}ms...`); await wait(backoff); } } // Fallback return fetchFn(input, init); }; }