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; 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) { async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) {
let url; let url;
try { try {
@ -95,32 +98,50 @@ async function fetchAccountsAndTransactions(accessUrl, sinceEpoch) {
const baseUrl = `${url.protocol}//${url.host}${url.pathname.replace(/\/?$/, '')}`; const baseUrl = `${url.protocol}//${url.host}${url.pathname.replace(/\/?$/, '')}`;
const endpoint = `${baseUrl}/accounts?start-date=${Math.floor(sinceEpoch)}&version=2`; const endpoint = `${baseUrl}/accounts?start-date=${Math.floor(sinceEpoch)}&version=2`;
let data; let lastErr;
try { for (let attempt = 0; attempt < FETCH_RETRY_ATTEMPTS; attempt++) {
const res = await fetch(endpoint, { if (attempt > 0) await new Promise(r => setTimeout(r, FETCH_RETRY_DELAYS[attempt - 1]));
headers: { 'Authorization': `Basic ${basicAuth}` },
signal: AbortSignal.timeout(30000), 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) { if (res.status === 403) {
throw Object.assign(new Error('SimpleFIN access has been revoked — please reconnect'), { code: 'SIMPLEFIN_REVOKED' }); throw Object.assign(new Error('SimpleFIN access has been revoked — please reconnect'), { code: 'SIMPLEFIN_REVOKED' });
} }
if (!res.ok) { 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) { let data;
throw sanitizeError(err); 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 throw sanitizeError(lastErr);
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;
} }
function normalizeAccount(rawAccount, dataSourceId, userId) { function normalizeAccount(rawAccount, dataSourceId, userId) {