Skip to content

Commit c01f398

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 c01f398

File tree

4 files changed

+453
-41
lines changed

4 files changed

+453
-41
lines changed

dapp/src/telegramService.js

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ 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-
{
10+
11+
const sendMessage = async () => {
12+
// wait 1 second before each call to avoid rate limit
13+
await new Promise((resolve) => {
14+
setTimeout(resolve, 1000);
15+
});
16+
17+
return fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
1118
method: 'POST',
1219
headers: {
1320
'Content-Type': 'application/json',
@@ -17,14 +24,69 @@ async function sendTelegram({
1724
text: messageToSend,
1825
parse_mode: 'HTML',
1926
}),
27+
});
28+
};
29+
30+
// retry logic with exponential backoff for handling rate limits (429) and network errors
31+
// eslint-disable-next-line no-plusplus
32+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
33+
try {
34+
// eslint-disable-next-line no-await-in-loop
35+
const response = await sendMessage();
36+
37+
if (response.ok) {
38+
return;
39+
}
40+
41+
if (response.status === 429) {
42+
const retryAfter = response.headers.get('Retry-After');
43+
const delay = retryAfter
44+
? parseInt(retryAfter, 10) * 1000
45+
: initialDelay * 2 ** attempt;
46+
47+
if (attempt < maxRetries) {
48+
// eslint-disable-next-line no-await-in-loop
49+
await new Promise((resolve) => {
50+
setTimeout(resolve, delay);
51+
});
52+
// eslint-disable-next-line no-continue
53+
continue;
54+
}
55+
56+
throw new Error(
57+
`Failed to send Telegram message: Rate limit exceeded after ${
58+
maxRetries + 1
59+
} attempts`
60+
);
61+
}
62+
63+
// other HTTP errors - throw directly, no retry
64+
throw new Error(
65+
`Failed to send Telegram message, bot API answered with status: ${response.status}`
66+
);
67+
} catch (error) {
68+
// if it's an HTTP error (404, 400, etc.) or rate limit error, re-throw immediately
69+
if (
70+
error.message.includes('Rate limit') ||
71+
error.message.includes('Failed to send')
72+
) {
73+
throw error;
74+
}
75+
76+
// network errors - retry with exponential backoff
77+
if (attempt < maxRetries) {
78+
const delay = initialDelay * 2 ** attempt;
79+
// eslint-disable-next-line no-await-in-loop
80+
await new Promise((resolve) => {
81+
setTimeout(resolve, delay);
82+
});
83+
// eslint-disable-next-line no-continue
84+
continue;
85+
}
86+
87+
// max retries reached for network errors
88+
throw new Error('Failed to reach Telegram bot API');
2089
}
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}`
27-
);
2890
}
2991
}
3092

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
{
2-
"success": false,
3-
"error": "Partial failure",
4-
"totalCount": 2,
5-
"successCount": 1,
6-
"errorCount": 1,
2+
"success": true,
3+
"totalCount": 10,
4+
"successCount": 10,
5+
"errorCount": 0,
76
"results": [
87
{
98
"index": 1,
@@ -12,9 +11,48 @@
1211
},
1312
{
1413
"index": 2,
15-
"protectedData": "invalid-data.zip",
16-
"success": false,
17-
"error": "Failed to parse ProtectedData 2: Failed to load protected data"
14+
"protectedData": "data-chatId.zip",
15+
"success": true
16+
},
17+
{
18+
"index": 3,
19+
"protectedData": "data-chatId.zip",
20+
"success": true
21+
},
22+
{
23+
"index": 4,
24+
"protectedData": "data-chatId.zip",
25+
"success": true
26+
},
27+
{
28+
"index": 5,
29+
"protectedData": "data-chatId.zip",
30+
"success": true
31+
},
32+
{
33+
"index": 6,
34+
"protectedData": "data-chatId.zip",
35+
"success": true
36+
},
37+
{
38+
"index": 7,
39+
"protectedData": "data-chatId.zip",
40+
"success": true
41+
},
42+
{
43+
"index": 8,
44+
"protectedData": "data-chatId.zip",
45+
"success": true
46+
},
47+
{
48+
"index": 9,
49+
"protectedData": "data-chatId.zip",
50+
"success": true
51+
},
52+
{
53+
"index": 10,
54+
"protectedData": "data-chatId.zip",
55+
"success": true
1856
}
1957
]
2058
}

dapp/tests/e2e/app.test.js

Lines changed: 65 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,36 @@ 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+
// eslint-disable-next-line no-plusplus
228+
for (let i = 1; i <= 5; i += 1) {
229+
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
230+
}
231+
232+
await expect(start()).resolves.toBeUndefined();
233+
234+
const { IEXEC_OUT } = process.env;
235+
const { result, computed } = await readOutputs(IEXEC_OUT);
236+
237+
expect(result.success).toBe(true);
238+
expect(result.totalCount).toBe(5);
239+
expect(result.successCount).toBe(5);
240+
expect(result.errorCount).toBe(0);
241+
242+
// Verify all messages were sent successfully
243+
result.results.forEach((r) => {
244+
expect(r.success).toBe(true);
245+
});
246+
247+
expect(computed).toStrictEqual({
248+
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
249+
});
250+
}, 30000); // 30 seconds timeout for 5 messages with delays
251+
});
252+
223253
describe('Bulk Processing', () => {
224254
beforeEach(() => {
225255
// Setup bulk processing environment
@@ -294,4 +324,38 @@ describe('sendTelegram', () => {
294324
});
295325
});
296326
});
327+
328+
describe('Retry mechanism with rate limiting', () => {
329+
it('should successfully send 10 Telegram messages with 1 second delay and handle retries', async () => {
330+
// Setup bulk processing with 10 protected data to test rate limiting and retries
331+
process.env.IEXEC_BULK_SLICE_SIZE = '10';
332+
333+
// eslint-disable-next-line no-plusplus
334+
for (let i = 1; i <= 10; i += 1) {
335+
process.env[`IEXEC_DATASET_${i}_FILENAME`] = 'data-chatId.zip';
336+
}
337+
338+
await expect(start()).resolves.toBeUndefined();
339+
340+
const { IEXEC_OUT } = process.env;
341+
const { result, computed } = await readOutputs(IEXEC_OUT);
342+
343+
expect(result.success).toBe(true);
344+
expect(result.totalCount).toBe(10);
345+
expect(result.successCount).toBe(10);
346+
expect(result.errorCount).toBe(0);
347+
expect(result.results).toHaveLength(10);
348+
349+
// Verify all messages were sent successfully
350+
result.results.forEach((r, index) => {
351+
expect(r.success).toBe(true);
352+
expect(r.index).toBe(index + 1);
353+
expect(r.protectedData).toBe('data-chatId.zip');
354+
});
355+
356+
expect(computed).toStrictEqual({
357+
'deterministic-output-path': `${IEXEC_OUT}/result.json`,
358+
});
359+
}, 60000); // 60 seconds timeout for 10 messages with delays and potential retries
360+
});
297361
});

0 commit comments

Comments
 (0)