JS: Fix HTTP Error 429

This commit is contained in:
gerlofvanek 2025-01-17 20:15:58 +01:00
parent fe02441619
commit 765ef9571a
2 changed files with 177 additions and 122 deletions
basicswap/static/js

View file

@ -74,7 +74,7 @@ let filterTimeout = null;
// CONFIGURATION CONSTANTS // CONFIGURATION CONSTANTS
// Time Constants // Time Constants
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes const CACHE_DURATION = 10 * 60 * 1000;
// Application Constants // Application Constants
const itemsPerPage = 50; const itemsPerPage = 50;
@ -1106,7 +1106,20 @@ function getEmptyPriceData() {
async function fetchLatestPrices() { async function fetchLatestPrices() {
const PRICES_CACHE_KEY = 'prices_coingecko'; const PRICES_CACHE_KEY = 'prices_coingecko';
const minRequestInterval = 15000;
const currentTime = Date.now();
if (!window.isManualRefresh) {
const lastRequestTime = window.lastPriceRequest || 0;
if (currentTime - lastRequestTime < minRequestInterval) {
console.log('Request too soon, using cache');
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
return cachedData.value;
}
}
}
window.lastPriceRequest = currentTime;
if (!window.isManualRefresh) { if (!window.isManualRefresh) {
const cachedData = CacheManager.get(PRICES_CACHE_KEY); const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData && cachedData.remainingTime > 60000) { if (cachedData && cachedData.remainingTime > 60000) {
@ -1120,10 +1133,11 @@ async function fetchLatestPrices() {
return cachedData.value; return cachedData.value;
} }
} }
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
try { try {
console.log('Initiating fresh price data fetch...'); console.log('Initiating fresh price data fetch...');
const existingCache = CacheManager.get(PRICES_CACHE_KEY, true);
let fallbackData = existingCache ? existingCache.value : null;
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
const response = await fetch('/json/readurl', { const response = await fetch('/json/readurl', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -1131,23 +1145,26 @@ async function fetchLatestPrices() {
}, },
body: JSON.stringify({ body: JSON.stringify({
url: url, url: url,
headers: {} headers: {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.5'
}
}) })
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
if (data.Error) { if (data.Error) {
console.error('API Error:', data.Error); if (fallbackData) {
console.log('Using fallback data due to API error');
return fallbackData;
}
throw new Error(data.Error); throw new Error(data.Error);
} }
if (data && Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
console.log('Processing fresh price data...'); console.log('Processing fresh price data');
latestPrices = data; latestPrices = data;
CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION); CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
@ -1156,14 +1173,27 @@ async function fetchLatestPrices() {
tableRateModule.setFallbackValue(coin, prices.usd); tableRateModule.setFallbackValue(coin, prices.usd);
} }
}); });
return data; return data;
} else {
console.warn('No price data received');
return null;
} }
if (fallbackData) {
console.log('Using fallback data due to empty response');
return fallbackData;
}
const fallbackPrices = {};
Object.keys(getEmptyPriceData()).forEach(coin => {
const fallbackValue = tableRateModule.getFallbackValue(coin);
if (fallbackValue !== null) {
fallbackPrices[coin] = { usd: fallbackValue, btc: null };
}
});
if (Object.keys(fallbackPrices).length > 0) {
return fallbackPrices;
}
console.warn('No price data received');
return null;
} catch (error) { } catch (error) {
console.error('Price Fetch Error:', error); console.error('Price Fetch Error:', error);
const fallbackPrices = {}; const fallbackPrices = {};
Object.keys(getEmptyPriceData()).forEach(coin => { Object.keys(getEmptyPriceData()).forEach(coin => {
const fallbackValue = tableRateModule.getFallbackValue(coin); const fallbackValue = tableRateModule.getFallbackValue(coin);
@ -2523,21 +2553,20 @@ function initializeTableEvents() {
}); });
} }
const refreshButton = document.getElementById('refreshOffers'); const refreshButton = document.getElementById('refreshOffers');
if (refreshButton) { if (refreshButton) {
EventManager.add(refreshButton, 'click', async () => { EventManager.add(refreshButton, 'click', async () => {
console.log('Manual refresh initiated'); console.log('Manual refresh initiated');
const refreshIcon = document.getElementById('refreshIcon'); const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText'); const refreshText = document.getElementById('refreshText');
refreshButton.disabled = true; refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin'); refreshIcon.classList.add('animate-spin');
refreshText.textContent = 'Refreshing...'; refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait'); refreshButton.classList.add('opacity-75', 'cursor-wait');
try { try {
const PRICES_CACHE_KEY = 'prices_coingecko'; const cachedPrices = CacheManager.get('prices_coingecko');
localStorage.removeItem(PRICES_CACHE_KEY); let previousPrices = cachedPrices ? cachedPrices.value : null;
CacheManager.clear(); CacheManager.clear();
window.isManualRefresh = true; window.isManualRefresh = true;
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
@ -2547,43 +2576,32 @@ if (refreshButton) {
} }
const newData = await response.json(); const newData = await response.json();
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
console.log('Fetched offers:', processedNewData.length);
jsonData = formatInitialData(processedNewData); jsonData = formatInitialData(processedNewData);
originalJsonData = [...jsonData]; originalJsonData = [...jsonData];
const priceData = await fetchLatestPrices();
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; if (!priceData && previousPrices) {
console.log('Fetching fresh prices...'); console.log('Using previous price data after failed refresh');
const priceResponse = await fetch('/json/readurl', { latestPrices = previousPrices;
method: 'POST', await updateOffersTable();
headers: { 'Content-Type': 'application/json' }, } else if (priceData) {
body: JSON.stringify({ url: url, headers: {} }) latestPrices = priceData;
}); await updateOffersTable();
} else {
if (priceResponse.ok) { throw new Error('Unable to fetch price data');
const priceData = await priceResponse.json();
if (priceData && Object.keys(priceData).length > 0) {
console.log('Updating with fresh price data');
latestPrices = priceData;
CacheManager.set(PRICES_CACHE_KEY, priceData, CACHE_DURATION);
Object.entries(priceData).forEach(([coin, prices]) => {
if (prices.usd) {
tableRateModule.setFallbackValue(coin, prices.usd);
}
});
}
} }
await updateOffersTable();
updateJsonView(); updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
lastRefreshTime = Date.now(); lastRefreshTime = Date.now();
updateLastRefreshTime(); updateLastRefreshTime();
console.log('Manual refresh completed successfully'); console.log('Manual refresh completed successfully');
} catch (error) { } catch (error) {
console.error('Error during manual refresh:', error); console.error('Error during manual refresh:', error);
ui.displayErrorMessage('Failed to refresh offers. Please try again later.'); ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
const cachedData = CacheManager.get('prices_coingecko');
if (cachedData?.value) {
latestPrices = cachedData.value;
await updateOffersTable();
}
} finally { } finally {
window.isManualRefresh = false; window.isManualRefresh = false;
refreshButton.disabled = false; refreshButton.disabled = false;

View file

@ -28,7 +28,7 @@ const config = {
} }
}, },
showVolume: false, showVolume: false,
cacheTTL: 5 * 60 * 1000, // 5 minutes cacheTTL: 10 * 60 * 1000,
specialCoins: [''], specialCoins: [''],
resolutions: { resolutions: {
year: { days: 365, interval: 'month' }, year: { days: 365, interval: 'month' },
@ -124,106 +124,143 @@ const api = {
error: error.message error: error.message
})); }));
}, },
fetchCoinGeckoDataXHR: async () => { fetchCoinGeckoDataXHR: async () => {
const cacheKey = 'coinGeckoOneLiner'; const cacheKey = 'coinGeckoOneLiner';
const minRequestInterval = 15000;
const currentTime = Date.now();
const lastRequestTime = window.lastGeckoRequest || 0;
if (currentTime - lastRequestTime < minRequestInterval) {
console.log('Request too soon, using cache');
const cachedData = cache.get(cacheKey); const cachedData = cache.get(cacheKey);
if (cachedData) { if (cachedData) {
console.log('Using cached CoinGecko data');
return cachedData.value; return cachedData.value;
} }
}
window.lastGeckoRequest = currentTime;
const cachedData = cache.get(cacheKey);
if (cachedData) {
console.log('Using cached CoinGecko data');
return cachedData.value;
}
try {
const existingCache = localStorage.getItem(cacheKey);
let fallbackData = null;
if (existingCache) {
try {
const parsed = JSON.parse(existingCache);
fallbackData = parsed.value;
} catch (e) {
console.warn('Failed to parse existing cache:', e);
}
}
const coinIds = config.coins const coinIds = config.coins
.filter(coin => coin.usesCoinGecko) .filter(coin => coin.usesCoinGecko)
.map(coin => coin.name) .map(coin => coin.name)
.join(','); .join(',');
const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true&api_key=${config.apiKeys.coinGecko}`; const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true&api_key=${config.apiKeys.coinGecko}`;
//console.log(`Fetching data for multiple coins from CoinGecko: ${url}`); const response = await api.makePostRequest(url, {
try { 'User-Agent': 'Mozilla/5.0',
const data = await api.makePostRequest(url); 'Accept': 'application/json',
//console.log(`Raw CoinGecko data:`, data); 'Accept-Language': 'en-US,en;q=0.5'
if (typeof data !== 'object' || data === null) { });
throw new AppError(`Invalid data structure received from CoinGecko`); if (typeof response !== 'object' || response === null) {
if (fallbackData) {
console.log('Using fallback data due to invalid response');
return fallbackData;
} }
const transformedData = {}; throw new AppError(`Invalid data structure received from CoinGecko`);
Object.entries(data).forEach(([id, values]) => {
const coinConfig = config.coins.find(coin => coin.name === id);
const symbol = coinConfig?.symbol.toLowerCase() || id;
transformedData[symbol] = {
current_price: values.usd,
price_btc: values.btc,
total_volume: values.usd_24h_vol,
price_change_percentage_24h: values.usd_24h_change,
displayName: coinConfig?.displayName || coinConfig?.symbol || id
};
});
//console.log(`Transformed CoinGecko data:`, transformedData);
cache.set(cacheKey, transformedData);
return transformedData;
} catch (error) {
//console.error(`Error fetching CoinGecko data:`, error);
return { error: error.message };
} }
}, if (response.error || response.Error) {
if (fallbackData) {
console.log('Using fallback data due to API error');
return fallbackData;
}
throw new AppError(response.error || response.Error);
}
const transformedData = {};
Object.entries(response).forEach(([id, values]) => {
const coinConfig = config.coins.find(coin => coin.name === id);
const symbol = coinConfig?.symbol.toLowerCase() || id;
transformedData[symbol] = {
current_price: values.usd,
price_btc: values.btc,
total_volume: values.usd_24h_vol,
price_change_percentage_24h: values.usd_24h_change,
displayName: coinConfig?.displayName || coinConfig?.symbol || id
};
});
cache.set(cacheKey, transformedData);
return transformedData;
fetchHistoricalDataXHR: async (coinSymbols) => { } catch (error) {
if (!Array.isArray(coinSymbols)) { console.error(`Error fetching CoinGecko data:`, error);
coinSymbols = [coinSymbols]; if (cachedData) {
console.log('Using expired cache data due to error');
return cachedData.value;
}
return { error: error.message };
} }
},
//console.log(`Fetching historical data for coins: ${coinSymbols.join(', ')}`); fetchHistoricalDataXHR: async (coinSymbols) => {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
const results = {}; const results = {};
const fetchPromises = coinSymbols.map(async coin => { const fetchPromises = coinSymbols.map(async coin => {
const coinConfig = config.coins.find(c => c.symbol === coin); const coinConfig = config.coins.find(c => c.symbol === coin);
if (!coinConfig) { if (!coinConfig) return;
//console.error(`Coin configuration not found for ${coin}`);
return;
}
if (coin === 'WOW') { const cacheKey = `historical_${coin}_${config.currentResolution}`;
const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${config.apiKeys.coinGecko}`; const cachedData = cache.get(cacheKey);
//console.log(`CoinGecko URL for WOW: ${url}`); if (cachedData) {
results[coin] = cachedData.value;
try { return;
const response = await api.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
} else {
//console.error(`Unexpected data structure for WOW:`, response);
}
} catch (error) {
console.error(`Error fetching CoinGecko data for WOW:`, error);
} }
} else { if (coin === 'WOW') {
const resolution = config.resolutions[config.currentResolution]; const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${config.apiKeys.coinGecko}`;
let url; try {
if (resolution.interval === 'hourly') { const response = await api.makePostRequest(url);
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`; if (response && response.prices) {
results[coin] = response.prices;
cache.set(cacheKey, response.prices);
}
} catch (error) {
console.error(`Error fetching CoinGecko data for WOW:`, error);
if (cachedData) {
results[coin] = cachedData.value;
}
}
} else { } else {
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`; const resolution = config.resolutions[config.currentResolution];
let url;
if (resolution.interval === 'hourly') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`;
} else {
url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`;
}
try {
const response = await api.makePostRequest(url);
if (response.Response === "Error") {
console.error(`API Error for ${coin}:`, response.Message);
if (cachedData) {
results[coin] = cachedData.value;
}
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
cache.set(cacheKey, response.Data);
}
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
if (cachedData) {
results[coin] = cachedData.value;
}
}
} }
//console.log(`CryptoCompare URL for ${coin}: ${url}`);
try {
const response = await api.makePostRequest(url);
if (response.Response === "Error") {
//console.error(`API Error for ${coin}:`, response.Message);
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
} else {
//console.error(`Unexpected data structure for ${coin}:`, response);
}
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
}
}
}); });
await Promise.all(fetchPromises); await Promise.all(fetchPromises);
//console.log('Final results object:', JSON.stringify(results, null, 2));
return results; return results;
} }
}; };
// CACHE // CACHE