fix(simplefin): retry transient fetch failures (3 attempts, 1s/2s backoff)

This commit is contained in:
null 2026-06-06 15:06:12 -05:00
parent a66fe13bc6
commit 7d42d119c0
1 changed files with 40 additions and 19 deletions

View File

@ -83,6 +83,9 @@ async function claimSetupToken(setupToken) {
return accessUrl;
}
const FETCH_RETRY_ATTEMPTS = 3;
const FETCH_RETRY_DELAYS = [1000, 2000]; // ms between attempts 1→2 and 2→3
async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) {
let url;
try {
@ -95,32 +98,50 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) {
const baseUrl = `${url.protocol}//${url.host}${url.pathname.replace(/\/?$/, '')}`;
const endpoint = `${baseUrl}/accounts?start-date=${Math.floor(sinceEpoch)}&version=2`;
let data;
try {
const res = await fetch(endpoint, {
headers: { 'Authorization': `Basic ${basicAuth}` },
signal: AbortSignal.timeout(30000),
});
let lastErr;
for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {
if (attempt > 0) await new Promise(r => setTimeout(r, FETCH_RETRY_DELAYS[attempt - 1]));
let res;
try {
res = await fetch(endpoint, {
headers: { 'Authorization': `Basic ${basicAuth}` },
signal: AbortSignal.timeout(30000),
});
} catch (err) {
// Network error or timeout — retry unless this was the last attempt
if (attempt < FETCH_RETRY_ATTEMPTS - 1) { lastErr = err; continue; }
throw sanitizeError(err);
}
if (res.status === 403) {
throw Object.assign(new Error('SimpleFIN access has been revoked — please reconnect'), { code: 'SIMPLEFIN_REVOKED' });
}
if (!res.ok) {
throw new Error(`SimpleFIN fetch failed (HTTP ${res.status})`);
const err = new Error(`SimpleFIN fetch failed (HTTP ${res.status})`);
// Retry transient server errors; surface client errors immediately
if (attempt < FETCH_RETRY_ATTEMPTS - 1 && res.status >= 500) { lastErr = err; continue; }
throw sanitizeError(err);
}
data = await res.json();
} catch (err) {
throw sanitizeError(err);
let data;
try {
data = await res.json();
} catch (err) {
throw sanitizeError(err);
}
// Surface any connection-level errors from the errlist so callers can log them
if (Array.isArray(data.errlist) && data.errlist.length > 0) {
const msgs = data.errlist
.map(e => sanitizeErrorMessage(e.message || e.msg || e.code || 'Unknown error'))
.join('; ');
data._errlistSummary = msgs;
}
return data;
}
// Surface any connection-level errors from the errlist so callers can log them
if (Array.isArray(data.errlist) && data.errlist.length > 0) {
const msgs = data.errlist
.map(e => sanitizeErrorMessage(e.message || e.msg || e.code || 'Unknown error'))
.join('; ');
data._errlistSummary = msgs;
}
return data;
throw sanitizeError(lastErr);
}
function normalizeAccount(rawAccount, dataSourceId, userId) {