Skip to content

Commit 75bb691

Browse files
committed
Harden Wakatime API handler auth and resiliency
1 parent cf4b341 commit 75bb691

1 file changed

Lines changed: 70 additions & 9 deletions

File tree

api/wakatime.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,85 @@
1+
const WAKATIME_STATS_URL = 'https://wakatime.com/api/v1/users/current/stats/last_7_days';
2+
const REQUEST_TIMEOUT_MS = 8000;
3+
const MAX_RETRIES = 2;
4+
const INITIAL_BACKOFF_MS = 300;
5+
6+
function sleep(ms) {
7+
return new Promise((resolve) => setTimeout(resolve, ms));
8+
}
9+
10+
function shouldRetry(status) {
11+
return status === 429 || status >= 500;
12+
}
13+
14+
async function fetchWithRetry(url, options) {
15+
let lastError;
16+
17+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
18+
const controller = new AbortController();
19+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
20+
21+
try {
22+
const response = await fetch(url, {
23+
...options,
24+
signal: controller.signal,
25+
});
26+
27+
clearTimeout(timeout);
28+
29+
if (!response.ok && shouldRetry(response.status) && attempt < MAX_RETRIES) {
30+
const backoffMs = INITIAL_BACKOFF_MS * (2 ** attempt);
31+
await sleep(backoffMs);
32+
continue;
33+
}
34+
35+
return response;
36+
} catch (error) {
37+
clearTimeout(timeout);
38+
lastError = error;
39+
40+
if (attempt >= MAX_RETRIES) {
41+
throw error;
42+
}
43+
44+
const backoffMs = INITIAL_BACKOFF_MS * (2 ** attempt);
45+
await sleep(backoffMs);
46+
}
47+
}
48+
49+
throw lastError || new Error('Failed to fetch Wakatime data');
50+
}
51+
152
export default async function handler(req, res) {
2-
const apiKey = process.env.WAKATIME_API_KEY || process.env.VITE_WAKATIME_API_KEY;
53+
if (req.method !== 'GET') {
54+
res.setHeader('Allow', 'GET');
55+
return res.status(405).json({ error: 'Method Not Allowed' });
56+
}
57+
58+
const apiKey = process.env.WAKATIME_API_KEY;
359

460
if (!apiKey) {
5-
return res.status(500).json({ error: 'Server Configuration Error: Missing API Key' });
61+
return res.status(500).json({ error: 'Server Configuration Error' });
662
}
763

64+
const authToken = Buffer.from(`${apiKey}:`).toString('base64');
65+
866
try {
9-
const url = `https://wakatime.com/api/v1/users/current/stats/last_7_days?api_key=${apiKey}`;
10-
const response = await fetch(url);
67+
const response = await fetchWithRetry(WAKATIME_STATS_URL, {
68+
headers: {
69+
Authorization: `Basic ${authToken}`,
70+
},
71+
});
1172

1273
if (!response.ok) {
13-
const errorText = await response.text();
14-
return res.status(response.status).json({ error: 'Failed to fetch from Wakatime', details: errorText });
74+
return res.status(response.status).json({ error: 'Failed to fetch from Wakatime' });
1575
}
1676

1777
const data = await response.json();
18-
78+
1979
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate');
2080
return res.status(200).json(data);
2181
} catch (error) {
22-
return res.status(500).json({ error: 'Internal Server Error', details: error.message });
82+
console.error('Wakatime fetch error:', error);
83+
return res.status(502).json({ error: 'Unable to fetch Wakatime data' });
2384
}
24-
}
85+
}

0 commit comments

Comments
 (0)