@@ -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