Repository navigation
Expand file tree
/
Copy pathcoingecko.js
More file actions
108 lines (93 loc) · 3.42 KB
/
Copy pathcoingecko.js
File metadata and controls
108 lines (93 loc) · 3.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
export const config = { runtime: 'edge' };
const ALLOWED_CURRENCIES = ['usd', 'eur', 'gbp', 'jpy', 'cny', 'btc', 'eth'];
const MAX_COIN_IDS = 20;
const COIN_ID_PATTERN = /^[a-z0-9-]+$/;
// Simple in-memory cache for edge function (reset on cold start)
let cache = { data: null, timestamp: 0 };
const CACHE_TTL = 120 * 1000; // 2 minutes
function validateCoinIds(idsParam) {
if (!idsParam) return 'bitcoin,ethereum,solana';
const ids = idsParam.split(',')
.map(id => id.trim().toLowerCase())
.filter(id => COIN_ID_PATTERN.test(id) && id.length <= 50)
.slice(0, MAX_COIN_IDS);
return ids.length > 0 ? ids.join(',') : 'bitcoin,ethereum,solana';
}
function validateCurrency(val) {
const currency = (val || 'usd').toLowerCase();
return ALLOWED_CURRENCIES.includes(currency) ? currency : 'usd';
}
function validateBoolean(val, defaultVal) {
if (val === 'true' || val === 'false') return val;
return defaultVal;
}
export default async function handler(req) {
const url = new URL(req.url);
const ids = validateCoinIds(url.searchParams.get('ids'));
const vsCurrencies = validateCurrency(url.searchParams.get('vs_currencies'));
const include24hrChange = validateBoolean(url.searchParams.get('include_24hr_change'), 'true');
// Return cached data if fresh
const cacheKey = `${ids}:${vsCurrencies}:${include24hrChange}`;
if (cache.data && cache.key === cacheKey && Date.now() - cache.timestamp < CACHE_TTL) {
return new Response(cache.data, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=120, stale-while-revalidate=60',
'X-Cache': 'HIT',
},
});
}
try {
const geckoUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=${vsCurrencies}&include_24hr_change=${include24hrChange}`;
const response = await fetch(geckoUrl, {
headers: {
'Accept': 'application/json',
},
});
// If rate limited, return cached data if available
if (response.status === 429 && cache.data && cache.key === cacheKey) {
return new Response(cache.data, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=120, stale-while-revalidate=60',
'X-Cache': 'STALE',
},
});
}
const data = await response.text();
// Cache successful responses
if (response.ok) {
cache = { data, key: cacheKey, timestamp: Date.now() };
}
return new Response(data, {
status: response.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=120, stale-while-revalidate=60',
'X-Cache': 'MISS',
},
});
} catch (error) {
// Return cached data on error if available
if (cache.data && cache.key === cacheKey) {
return new Response(cache.data, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=120',
'X-Cache': 'ERROR-FALLBACK',
},
});
}
return new Response(JSON.stringify({ error: 'Failed to fetch data' }), {
status: 500,
headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
});
}
}