Skip to main content
Glama
retry-handler.test.ts10.3 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { withRetry, RetryOptions } from './retry-handler.js'; describe('Retry Handler', () => { beforeEach(() => { vi.useFakeTimers(); }); describe('Exponential Backoff', () => { it('should retry with exponential backoff delays', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts < 3) { const error: any = new Error('Server error'); error.response = { status: 500 }; throw error; } return 'success'; }); const promise = withRetry(fn); // First attempt - immediate await vi.runOnlyPendingTimersAsync(); // Second attempt - 1000ms delay await vi.advanceTimersByTimeAsync(1000); // Third attempt - 2000ms delay await vi.advanceTimersByTimeAsync(2000); const result = await promise; expect(result.ok).toBe(true); if (result.ok) { expect(result.value).toBe('success'); } expect(fn).toHaveBeenCalledTimes(3); }); it('should use custom base delay', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts < 2) { const error: any = new Error('Server error'); error.response = { status: 500 }; throw error; } return 'success'; }); const options: RetryOptions = { baseDelay: 500 }; const promise = withRetry(fn, options); await vi.runOnlyPendingTimersAsync(); await vi.advanceTimersByTimeAsync(500); // Custom base delay const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); }); describe('5xx Error Handling', () => { it('should retry on 500 Internal Server Error', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Server error'); error.response = { status: 500 }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); it('should retry on 502 Bad Gateway', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Bad Gateway'); error.response = { status: 502 }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); it('should retry on 503 Service Unavailable', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Service Unavailable'); error.response = { status: 503 }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); it('should retry on 504 Gateway Timeout', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Gateway Timeout'); error.response = { status: 504 }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); }); describe('4xx Error Handling', () => { it('should NOT retry on 400 Bad Request', async () => { const fn = vi.fn(async () => { const error: any = new Error('Bad Request'); error.response = { status: 400 }; throw error; }); const result = await withRetry(fn); expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(1); }); it('should NOT retry on 401 Unauthorized', async () => { const fn = vi.fn(async () => { const error: any = new Error('Unauthorized'); error.response = { status: 401 }; throw error; }); const result = await withRetry(fn); expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(1); }); it('should NOT retry on 404 Not Found', async () => { const fn = vi.fn(async () => { const error: any = new Error('Not Found'); error.response = { status: 404 }; throw error; }); const result = await withRetry(fn); expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(1); }); }); describe('429 Rate Limit Handling', () => { it('should retry after Retry-After header value (seconds)', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Too Many Requests'); error.response = { status: 429, headers: { 'retry-after': '5' }, }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runOnlyPendingTimersAsync(); await vi.advanceTimersByTimeAsync(5000); // Retry-After: 5 seconds const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); it('should use default delay when Retry-After header is missing', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { const error: any = new Error('Too Many Requests'); error.response = { status: 429 }; throw error; } return 'success'; }); const promise = withRetry(fn); await vi.runOnlyPendingTimersAsync(); await vi.advanceTimersByTimeAsync(1000); // Default base delay const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); }); describe('Max Attempts', () => { it('should stop retrying after max attempts (default 3)', async () => { const fn = vi.fn(async () => { const error: any = new Error('Server error'); error.response = { status: 500 }; throw error; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(3); if (!result.ok) { expect(result.error.attempts).toBe(3); expect(result.error.lastError).toBeDefined(); } }); it('should respect custom max attempts', async () => { const fn = vi.fn(async () => { const error: any = new Error('Server error'); error.response = { status: 500 }; throw error; }); const options: RetryOptions = { maxAttempts: 5 }; const promise = withRetry(fn, options); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(5); }); it('should return error details on retry exhaustion', async () => { const testError: any = new Error('Persistent error'); testError.response = { status: 500 }; const fn = vi.fn(async () => { throw testError; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.attempts).toBe(3); expect(result.error.lastError).toBe(testError); } }); }); describe('Custom Retry Logic', () => { it('should use custom shouldRetry function', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts === 1) { throw new Error('Custom retryable error'); } return 'success'; }); const options: RetryOptions = { shouldRetry: (error: unknown) => { return error instanceof Error && error.message.includes('retryable'); }, }; const promise = withRetry(fn, options); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); expect(fn).toHaveBeenCalledTimes(2); }); it('should not retry when custom shouldRetry returns false', async () => { const fn = vi.fn(async () => { throw new Error('Non-retryable error'); }); const options: RetryOptions = { shouldRetry: () => false, }; const result = await withRetry(fn, options); expect(result.ok).toBe(false); expect(fn).toHaveBeenCalledTimes(1); }); }); describe('Successful Execution', () => { it('should return success on first attempt', async () => { const fn = vi.fn(async () => 'immediate success'); const result = await withRetry(fn); expect(result.ok).toBe(true); if (result.ok) { expect(result.value).toBe('immediate success'); } expect(fn).toHaveBeenCalledTimes(1); }); it('should return success after retries', async () => { let attempts = 0; const fn = vi.fn(async () => { attempts++; if (attempts < 3) { const error: any = new Error('Temporary error'); error.response = { status: 503 }; throw error; } return 'eventual success'; }); const promise = withRetry(fn); await vi.runAllTimersAsync(); const result = await promise; expect(result.ok).toBe(true); if (result.ok) { expect(result.value).toBe('eventual success'); } expect(fn).toHaveBeenCalledTimes(3); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ssoma-dev/mcp-server-lychee-redmine'

If you have feedback or need assistance with the MCP directory API, please join our Discord server