feat: implement auto-retry for Telegram API requests on error 429 and update documentation
This commit is contained in:
+4
-65
@@ -9,79 +9,18 @@ const warnSpy = spyOn(logger, 'warn');
|
||||
describe('Rate Limiter', () => {
|
||||
beforeEach(() => {
|
||||
warnSpy.mockClear();
|
||||
// Set custom env variables for predictable tests
|
||||
process.env.RATE_LIMIT_WINDOW_MS = '100'; // 100ms window
|
||||
process.env.RATE_LIMIT_MAX_REQUESTS = '3'; // max 3 requests
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.RATE_LIMIT_WINDOW_MS;
|
||||
delete process.env.RATE_LIMIT_MAX_REQUESTS;
|
||||
});
|
||||
|
||||
it('should allow requests under the limit', () => {
|
||||
it('should always allow requests as rate limiter is disabled', () => {
|
||||
const key = 'user-1';
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should block requests exceeding the limit and log a warning', () => {
|
||||
const key = 'user-2';
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
|
||||
// 4th request exceeds limit of 3
|
||||
expect(checkRateLimit(key)).toBe(false);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
const callArgs = warnSpy.mock.calls[0];
|
||||
expect(callArgs[0]).toBe('Rate limit exceeded');
|
||||
expect(callArgs[1].key).toBe(key);
|
||||
});
|
||||
|
||||
it('should reset request count after the window passes', async () => {
|
||||
const key = 'user-3';
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
expect(checkRateLimit(key)).toBe(false); // blocked
|
||||
|
||||
// Wait for window to expire (100ms)
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
|
||||
// Should be allowed again
|
||||
expect(checkRateLimit(key)).toBe(true);
|
||||
});
|
||||
|
||||
it('should cleanup rate limit cache of expired keys', async () => {
|
||||
const key1 = 'cleanup-1';
|
||||
const key2 = 'cleanup-2';
|
||||
|
||||
// Populate keys
|
||||
expect(checkRateLimit(key1)).toBe(true);
|
||||
expect(checkRateLimit(key2)).toBe(true);
|
||||
|
||||
// Run cleanup immediately (none should be expired yet as 100ms hasn't passed)
|
||||
cleanupRateLimitCache();
|
||||
|
||||
// Verify still tracked (counts shouldn't reset, e.g., if we consume remaining limits)
|
||||
expect(checkRateLimit(key1)).toBe(true); // request 2
|
||||
expect(checkRateLimit(key1)).toBe(true); // request 3
|
||||
expect(checkRateLimit(key1)).toBe(false); // request 4 (blocked)
|
||||
|
||||
// Wait for window to expire
|
||||
await new Promise((resolve) => setTimeout(resolve, 110));
|
||||
|
||||
// Run cleanup
|
||||
cleanupRateLimitCache();
|
||||
|
||||
// Since they were deleted from the map, they should be initialized as new records
|
||||
// If they were cleaned up, we should be able to do 3 requests again
|
||||
expect(checkRateLimit(key1)).toBe(true); // 1
|
||||
expect(checkRateLimit(key1)).toBe(true); // 2
|
||||
expect(checkRateLimit(key1)).toBe(true); // 3
|
||||
expect(checkRateLimit(key1)).toBe(false); // 4 (blocked)
|
||||
it('should no-op on cleanup', () => {
|
||||
expect(() => cleanupRateLimitCache()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,6 +115,36 @@ describe('Telegram API Utilities', () => {
|
||||
error: 'Telegram send failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry when telegram returns 429 Too Many Requests', async () => {
|
||||
const bot = getBot();
|
||||
let calls = 0;
|
||||
bot.telegram.sendPhoto = mock(() => {
|
||||
calls++;
|
||||
if (calls === 1) {
|
||||
return Promise.reject(new Error('429: Too Many Requests: retry after 1'));
|
||||
}
|
||||
return Promise.resolve({
|
||||
message_id: 999,
|
||||
photo: [{ file_id: 'retry_photo_id', file_unique_id: 'retry_unique_id' }],
|
||||
});
|
||||
});
|
||||
|
||||
const chunk = Buffer.from('fake photo data');
|
||||
const fileName = 'test_photo.jpg';
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await forwardToStorage(chunk, fileName, 'photo');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(calls).toBe(2);
|
||||
expect(duration).toBeGreaterThanOrEqual(1000);
|
||||
expect(result).toEqual({
|
||||
telegramFileId: 'retry_photo_id',
|
||||
telegramFileUniqueId: 'retry_unique_id',
|
||||
storageMessageId: 999,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileInfo', () => {
|
||||
|
||||
Reference in New Issue
Block a user