Skip to content

Commit ea8c620

Browse files
committed
Add more robust error handling to SwipeChart
1 parent 514ea6a commit ea8c620

2 files changed

Lines changed: 77 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased (develop)
44

5+
- added: More robust error handling in `SwipeChart` to handle rate limits
6+
57
## 4.44.0 (staging)
68

79
- added: MAYAChain (CACAO) wallet support

src/components/charts/SwipeChart.tsx

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -286,49 +286,84 @@ export const SwipeChart: React.FC<Props> = props => {
286286
)
287287
// Start with the free base URL
288288
let fetchUrl = `${COINGECKO_URL}${fetchPath}`
289-
do {
290-
try {
291-
// Construct the dataset query
292-
const response = await fetch(fetchUrl)
293-
const result = await response.json()
294-
const apiError = asMaybe(asCoinGeckoError)(result)
295-
if (apiError != null) {
296-
if (apiError.status.error_code === 429) {
297-
// Rate limit error, use our API key as a fallback
298-
if (
299-
!fetchUrl.includes('x_cg_pro_api_key') &&
300-
ENV.COINGECKO_API_KEY !== ''
301-
) {
302-
fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}`
289+
let retryCount = 0
290+
const maxRetries = 3
291+
try {
292+
do {
293+
try {
294+
const response = await fetch(fetchUrl)
295+
296+
// Handle non-OK responses before parsing JSON
297+
if (!response.ok) {
298+
if (response.status === 429) {
299+
if (
300+
!fetchUrl.includes('x_cg_pro_api_key') &&
301+
ENV.COINGECKO_API_KEY !== ''
302+
) {
303+
fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}`
304+
}
305+
retryCount++
306+
if (retryCount >= maxRetries) {
307+
throw new Error(`Rate limited after ${maxRetries} retries`)
308+
}
309+
await snooze(2000)
310+
continue
303311
}
304-
// Wait 2 second before retrying. It typically takes 1 minute
305-
// before rate limiting is relieved, so even 2 seconds is hasty.
306-
await snooze(2000)
307-
continue
312+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
308313
}
309-
throw new Error(
310-
`Failed to fetch market data: ${apiError.status.error_code} ${apiError.status.error_message}`
311-
)
312-
}
313314

314-
const marketChartRange = asCoinGeckoMarketChartRange(result)
315-
const rawChartData = marketChartRange.prices.map(pair => ({
316-
x: new Date(pair[0]),
317-
y: pair[1]
318-
}))
319-
const reduced = reduceChartData(rawChartData, selectedTimespan)
320-
321-
setChartData(reduced)
322-
cachedTimespanChartData.set(selectedTimespan, reduced)
323-
setCachedChartData(cachedTimespanChartData)
324-
} catch (e: unknown) {
325-
console.error(JSON.stringify(e))
326-
setErrorMessage(lstrings.error_data_unavailable)
327-
} finally {
328-
setIsFetching(false)
329-
}
330-
break
331-
} while (true)
315+
// Parse JSON safely - use text() first to catch parse errors locally
316+
const text = await response.text()
317+
let result: unknown
318+
try {
319+
result = JSON.parse(text)
320+
} catch {
321+
throw new Error(`Invalid JSON response: ${text.slice(0, 100)}...`)
322+
}
323+
324+
const apiError = asMaybe(asCoinGeckoError)(result)
325+
if (apiError != null) {
326+
if (apiError.status.error_code === 429) {
327+
if (
328+
!fetchUrl.includes('x_cg_pro_api_key') &&
329+
ENV.COINGECKO_API_KEY !== ''
330+
) {
331+
fetchUrl = `${COINGECKO_URL_PRO}${fetchPath}&x_cg_pro_api_key=${ENV.COINGECKO_API_KEY}`
332+
}
333+
retryCount++
334+
if (retryCount >= maxRetries) {
335+
throw new Error(`Rate limited after ${maxRetries} retries`)
336+
}
337+
// Wait 2 seconds before retrying. It typically takes 1 minute
338+
// before rate limiting is relieved, so even 2 seconds is hasty.
339+
await snooze(2000)
340+
continue
341+
}
342+
throw new Error(
343+
`Failed to fetch market data: ${apiError.status.error_code} ${apiError.status.error_message}`
344+
)
345+
}
346+
347+
const marketChartRange = asCoinGeckoMarketChartRange(result)
348+
const rawChartData = marketChartRange.prices.map(pair => ({
349+
x: new Date(pair[0]),
350+
y: pair[1]
351+
}))
352+
const reduced = reduceChartData(rawChartData, selectedTimespan)
353+
354+
setChartData(reduced)
355+
cachedTimespanChartData.set(selectedTimespan, reduced)
356+
setCachedChartData(cachedTimespanChartData)
357+
} catch (e: unknown) {
358+
const message = e instanceof Error ? e.message : String(e)
359+
console.error('SwipeChart fetch error:', message)
360+
setErrorMessage(lstrings.error_data_unavailable)
361+
}
362+
break
363+
} while (true)
364+
} finally {
365+
setIsFetching(false)
366+
}
332367
},
333368
[selectedTimespan, isConnected, fetchAssetId, coingeckoFiat],
334369
'swipeChart'

0 commit comments

Comments
 (0)