diff --git a/services/simplefinService.js b/services/simplefinService.js index 5cdd564..899fda8 100644 --- a/services/simplefinService.js +++ b/services/simplefinService.js @@ -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) {