Skip to content

Commit 47389cb

Browse files
fix(dapp): implement rate limiting to prevent 429 errors
- Add 1 second delay before each Telegram API call to respect rate limits - Increase maxRetries to 10 for better error recovery - Improve error handling: HTTP errors (non-429) fail immediately without retry - Only retry on 429 rate limit errors and network errors - Add unit and e2e tests for rate limiting behavior
1 parent 4ad8ac1 commit 47389cb

File tree

3 files changed

+404
-42
lines changed

3 files changed

+404
-42
lines changed

dapp/src/telegramService.js

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,83 @@ async function sendTelegram({
33
message,
44
botToken,
55
senderName = 'Web3Telegram Dapp',
6+
maxRetries = 10,
7+
initialDelay = 1000,
68
}) {
79
const messageToSend = `Message from: ${senderName}\n${message}`;
8-
const response = await fetch(
9-
`https://api.telegram.org/bot${botToken}/sendMessage`,
10-
{
11-
method: 'POST',
12-
headers: {
13-
'Content-Type': 'application/json',
14-
},
15-
body: JSON.stringify({
16-
chat_id: chatId,
17-
text: messageToSend,
18-
parse_mode: 'HTML',
19-
}),
20-
}
21-
).catch(() => {
22-
throw new Error('Failed to reach Telegram bot API');
23-
});
24-
if (!response.ok) {
25-
throw new Error(
26-
`Failed to send Telegram message, bot API answered with status: ${response.status}`
10+
11+
const sendMessage = async () => {
12+
// wait 1 second before each call to avoid rate limit
13+
await new Promise((resolve) => setTimeout(resolve, 1000));
14+
15+
const response = await fetch(
16+
`https://api.telegram.org/bot${botToken}/sendMessage`,
17+
{
18+
method: 'POST',
19+
headers: {
20+
'Content-Type': 'application/json',
21+
},
22+
body: JSON.stringify({
23+
chat_id: chatId,
24+
text: messageToSend,
25+
parse_mode: 'HTML',
26+
}),
27+
}
2728
);
29+
30+
return response;
31+
};
32+
33+
// retry logic with exponential backoff for handling rate limits (429) and network errors
34+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
35+
try {
36+
const response = await sendMessage();
37+
38+
if (response.ok) {
39+
return;
40+
}
41+
42+
if (response.status === 429) {
43+
const retryAfter = response.headers.get('Retry-After');
44+
const delay = retryAfter
45+
? parseInt(retryAfter) * 1000
46+
: initialDelay * Math.pow(2, attempt);
47+
48+
if (attempt < maxRetries) {
49+
await new Promise((resolve) => setTimeout(resolve, delay));
50+
continue;
51+
}
52+
53+
throw new Error(
54+
`Failed to send Telegram message: Rate limit exceeded after ${
55+
maxRetries + 1
56+
} attempts`
57+
);
58+
}
59+
60+
// other HTTP errors - throw directly, no retry
61+
throw new Error(
62+
`Failed to send Telegram message, bot API answered with status: ${response.status}`
63+
);
64+
} catch (error) {
65+
// if it's an HTTP error (404, 400, etc.) or rate limit error, re-throw immediately
66+
if (
67+
error.message.includes('Rate limit') ||
68+
error.message.includes('Failed to send')
69+
) {
70+
throw error;
71+
}
72+
73+
// network errors - retry with exponential backoff
74+
if (attempt < maxRetries) {
75+
const delay = initialDelay * Math.pow(2, attempt);
76+
await new Promise((resolve) => setTimeout(resolve, delay));
77+
continue;
78+
}
79+
80+
// max retries reached for network errors
81+
throw new Error('Failed to reach Telegram bot API');
82+
}
2883
}
2984
}
3085

dapp/tests/e2e/app.test.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe('sendTelegram', () => {
206206
expect(computed).toStrictEqual({
207207
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
208208
});
209-
});
209+
}, 10000);
210210

211211
it('should send a telegram message successfully', async () => {
212212
await expect(start()).resolves.toBeUndefined();
@@ -220,6 +220,35 @@ describe('sendTelegram', () => {
220220
});
221221
});
222222

223+
describe('Rate Limiting', () => {
224+
it('should handle multiple messages with 1 second delay between calls', async () => {
225+
// Setup bulk processing with 5 messages to test rate limiting
226+
process.env.IEXEC_BULK_SLICE_SIZE = '5';
227+
for (let i = 1; i <= 5; i++) {
228+
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
229+
}
230+
231+
await expect(start()).resolves.toBeUndefined();
232+
233+
const { IEXEC_OUT } = process.env;
234+
const { result, computed } = await readOutputs(IEXEC_OUT);
235+
236+
expect(result.success).toBe(true);
237+
expect(result.totalCount).toBe(5);
238+
expect(result.successCount).toBe(5);
239+
expect(result.errorCount).toBe(0);
240+
241+
// Verify all messages were sent successfully
242+
result.results.forEach((r) => {
243+
expect(r.success).toBe(true);
244+
});
245+
246+
expect(computed).toStrictEqual({
247+
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
248+
});
249+
}, 30000); // 30 seconds timeout for 5 messages with delays
250+
});
251+
223252
describe('Bulk Processing', () => {
224253
beforeEach(() => {
225254
// Setup bulk processing environment
@@ -294,4 +323,37 @@ describe('sendTelegram', () => {
294323
});
295324
});
296325
});
326+
327+
describe('Retry mechanism with rate limiting', () => {
328+
it('should successfully send 10 Telegram messages with 1 second delay and handle retries', async () => {
329+
// Setup bulk processing with 10 protected data to test rate limiting and retries
330+
process.env.IEXEC_BULK_SLICE_SIZE = '10';
331+
332+
for (let i = 1; i <= 10; i++) {
333+
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
334+
}
335+
336+
await expect(start()).resolves.toBeUndefined();
337+
338+
const { IEXEC_OUT } = process.env;
339+
const { result, computed } = await readOutputs(IEXEC_OUT);
340+
341+
expect(result.success).toBe(true);
342+
expect(result.totalCount).toBe(10);
343+
expect(result.successCount).toBe(10);
344+
expect(result.errorCount).toBe(0);
345+
expect(result.results).toHaveLength(10);
346+
347+
// Verify all messages were sent successfully
348+
result.results.forEach((r, index) => {
349+
expect(r.success).toBe(true);
350+
expect(r.index).toBe(index + 1);
351+
expect(r.protectedData).toBe('data-chatId.zip');
352+
});
353+
354+
expect(computed).toStrictEqual({
355+
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
356+
});
357+
}, 60000); // 60 seconds timeout for 10 messages with delays and potential retries
358+
});
297359
});

0 commit comments

Comments
 (0)