From 6fdde0508d2d551d88cc11ae8714d67c370fcfdb Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Mon, 23 Feb 2026 15:14:24 -0800 Subject: [PATCH] Add more robust error handling to `SwipeChart` --- CHANGELOG.md | 2 + eslint.config.mjs | 1 - src/components/charts/SwipeChart.tsx | 107 +++++++++++++++++---------- 3 files changed, 69 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df246a5f107..e46c75fb5fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased (develop) +- added: More robust error handling in `SwipeChart` to handle rate limits + ## 4.44.0 (staging) - added: MAYAChain (CACAO) wallet support diff --git a/eslint.config.mjs b/eslint.config.mjs index c20fb1371c6..07707445705 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -138,7 +138,6 @@ export default [ 'src/components/cards/VisaCardCard.tsx', 'src/components/cards/WalletRestoreCard.tsx', 'src/components/cards/WarningCard.tsx', - 'src/components/charts/SwipeChart.tsx', 'src/components/common/AnimatedNumber.tsx', 'src/components/common/BlurBackground.tsx', diff --git a/src/components/charts/SwipeChart.tsx b/src/components/charts/SwipeChart.tsx index 156f6aecf3a..4191451ed11 100644 --- a/src/components/charts/SwipeChart.tsx +++ b/src/components/charts/SwipeChart.tsx @@ -286,49 +286,76 @@ export const SwipeChart: React.FC = props => { ) // Start with the free base URL let fetchUrl = `${COINGECKO_URL}${fetchPath}` - do { - try { - // Construct the dataset query - const response = await fetch(fetchUrl) - const result = await response.json() - const apiError = asMaybe(asCoinGeckoError)(result) - if (apiError != null) { - if (apiError.status.error_code === 429) { - // Rate limit error, use our API key as a fallback - if ( - !fetchUrl.includes('x_cg_pro_api_key') && - ENV.COINGECKO_API_KEY !== '' - ) { - fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}` + try { + do { + try { + const response = await fetch(fetchUrl) + + // Handle non-OK responses before parsing JSON + if (!response.ok) { + if (response.status === 429) { + if ( + !fetchUrl.includes('x_cg_pro_api_key') && + ENV.COINGECKO_API_KEY !== '' + ) { + fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}` + } + // Wait 2 seconds before retrying. It typically takes 1 minute + // before rate limiting is relieved, so even 2 seconds is hasty. + await snooze(2000) + continue } - // Wait 2 second before retrying. It typically takes 1 minute - // before rate limiting is relieved, so even 2 seconds is hasty. - await snooze(2000) - continue + throw new Error(`HTTP ${response.status}: ${response.statusText}`) } - throw new Error( - `Failed to fetch market data: ${apiError.status.error_code} ${apiError.status.error_message}` - ) - } - const marketChartRange = asCoinGeckoMarketChartRange(result) - const rawChartData = marketChartRange.prices.map(pair => ({ - x: new Date(pair[0]), - y: pair[1] - })) - const reduced = reduceChartData(rawChartData, selectedTimespan) - - setChartData(reduced) - cachedTimespanChartData.set(selectedTimespan, reduced) - setCachedChartData(cachedTimespanChartData) - } catch (e: unknown) { - console.error(JSON.stringify(e)) - setErrorMessage(lstrings.error_data_unavailable) - } finally { - setIsFetching(false) - } - break - } while (true) + // Parse JSON safely - use text() first to catch parse errors locally + const text = await response.text() + let result: unknown + try { + result = JSON.parse(text) + } catch { + throw new Error(`Invalid JSON response: ${text.slice(0, 100)}...`) + } + + const apiError = asMaybe(asCoinGeckoError)(result) + if (apiError != null) { + if (apiError.status.error_code === 429) { + if ( + !fetchUrl.includes('x_cg_pro_api_key') && + ENV.COINGECKO_API_KEY !== '' + ) { + fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}` + } + // Wait 2 seconds before retrying. It typically takes 1 minute + // before rate limiting is relieved, so even 2 seconds is hasty. + await snooze(2000) + continue + } + throw new Error( + `Failed to fetch market data: ${apiError.status.error_code} ${apiError.status.error_message}` + ) + } + + const marketChartRange = asCoinGeckoMarketChartRange(result) + const rawChartData = marketChartRange.prices.map(pair => ({ + x: new Date(pair[0]), + y: pair[1] + })) + const reduced = reduceChartData(rawChartData, selectedTimespan) + + setChartData(reduced) + cachedTimespanChartData.set(selectedTimespan, reduced) + setCachedChartData(cachedTimespanChartData) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + console.error('SwipeChart fetch error:', message) + setErrorMessage(lstrings.error_data_unavailable) + } + break + } while (true) + } finally { + setIsFetching(false) + } }, [selectedTimespan, isConnected, fetchAssetId, coingeckoFiat], 'swipeChart'