Skip to content

Commit 5e7c287

Browse files
authored
Fix exponential backoff delay calculation in fetchWithRetry (#14159)
1 parent aede3cc commit 5e7c287

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

.changeset/proud-jeans-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
`fetchWithRetry` now backs off in sequence 2s → 4s → 8s and then caps at 10s.

packages/core/src/utils.test.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { jsonSchemaToZod } from '@mastra/schema-compat/json-to-zod';
2-
import { describe, expect, it, vi } from 'vitest';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
33
import { z } from 'zod';
44
import { MastraError } from './error';
55
import { ConsoleLogger } from './logger';
66
import { RequestContext } from './request-context';
77
import { toStandardSchema } from './schema';
88
import { createTool, isVercelTool } from './tools';
9-
import { makeCoreTool, maskStreamTags, resolveSerializedZodOutput } from './utils';
9+
import { fetchWithRetry, makeCoreTool, maskStreamTags, resolveSerializedZodOutput } from './utils';
1010

1111
describe('maskStreamTags', () => {
1212
async function* makeStream(chunks: string[]) {
@@ -344,3 +344,39 @@ it('should log correctly for Vercel tool execution', async () => {
344344

345345
debugSpy.mockRestore();
346346
});
347+
348+
describe('fetchWithRetry', () => {
349+
afterEach(() => {
350+
vi.restoreAllMocks();
351+
vi.unstubAllGlobals();
352+
});
353+
354+
it('should use exponential backoff delays capped at 10 seconds', async () => {
355+
const delays: number[] = [];
356+
357+
vi.spyOn(globalThis, 'setTimeout').mockImplementation(((fn: () => void, delay?: number) => {
358+
if (delay && delay > 100) {
359+
delays.push(delay);
360+
}
361+
// Execute callback immediately so the test completes
362+
if (typeof fn === 'function') fn();
363+
return 0 as unknown as ReturnType<typeof setTimeout>;
364+
}) as typeof setTimeout);
365+
366+
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'));
367+
vi.stubGlobal('fetch', mockFetch);
368+
369+
// Use 5 retries so computed backoff 1000 * 2^4 = 16000 exceeds the 10000 cap
370+
await expect(fetchWithRetry('https://example.com', {}, 5)).rejects.toThrow();
371+
372+
// Delays: 2000 (2^1), 4000 (2^2), 8000 (2^3), 10000 (2^4=16000 capped to 10000)
373+
expect(delays.length).toBe(4); // 5 max retries = 4 retry delays
374+
for (const delay of delays) {
375+
expect(delay).toBeLessThanOrEqual(10000);
376+
}
377+
expect(delays[0]).toBe(2000); // 1000 * 2^1
378+
expect(delays[1]).toBe(4000); // 1000 * 2^2
379+
expect(delays[2]).toBe(8000); // 1000 * 2^3
380+
expect(delays[3]).toBe(10000); // 1000 * 2^4 = 16000, capped at 10000
381+
});
382+
});

packages/core/src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ export async function fetchWithRetry(
638638
break;
639639
}
640640

641-
const delay = Math.min(1000 * Math.pow(2, retryCount) * 1000, 10000);
641+
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
642642
await new Promise(resolve => setTimeout(resolve, delay));
643643
}
644644
}

0 commit comments

Comments
 (0)