Skip to content

Commit 143034b

Browse files
authored
feat(retry-plugin): add getRetryPath in retry plugin to support users to customize retry path (#4023)
1 parent ffbce8e commit 143034b

File tree

11 files changed

+392
-174
lines changed

11 files changed

+392
-174
lines changed

.changeset/fuzzy-foxes-perform.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@module-federation/retry-plugin': patch
3+
'@module-federation/runtime-core': patch
4+
---
5+
6+
feat(retry-plugin): Add getRetryPath support in retry plugin to allow users to customize retry path

apps/router-demo/router-host-2000/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ErrorBoundary } from 'react-error-boundary';
1717
import Remote1AppNew from 'remote1/app';
1818
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
1919
import { Spin } from 'antd';
20+
import { createInstance } from '@module-federation/enhanced/runtime';
2021

2122
const fallbackPlugin: () => ModuleFederationRuntimePlugin = function () {
2223
return {
@@ -27,7 +28,7 @@ const fallbackPlugin: () => ModuleFederationRuntimePlugin = function () {
2728
};
2829
};
2930

30-
init({
31+
const mf = createInstance({
3132
name: 'federation_consumer',
3233
remotes: [],
3334
plugins: [

apps/router-demo/router-host-2000/src/runtime-plugin/retry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { RetryPlugin } from '@module-federation/retry-plugin';
33
const retryPlugin = () =>
44
RetryPlugin({
55
fetch: {
6-
// url: 'http://localhost:2001/mf-manifest.json',
76
// fallback: () => 'http://localhost:2001/mf-manifest.json',
7+
// getRetryPath: (url) => {
8+
// return 'http://localhost:2001/mf-manifest.json?test=1';
9+
// },
810
},
911
script: {
1012
retryTimes: 3,
@@ -20,6 +22,9 @@ const retryPlugin = () =>
2022
resolve(error);
2123
}, 1000);
2224
},
25+
getRetryPath: (url) => {
26+
return 'http://localhost:2001/remote1.js?test=2';
27+
},
2328
},
2429
});
2530
export default retryPlugin;

packages/retry-plugin/__tests__/retry.spec.ts

Lines changed: 179 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { fetchWithRetry } from '../src/fetch-retry';
2-
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
33

4-
// Mock the DOM environment
54
beforeEach(() => {
65
vi.clearAllMocks();
76
vi.useFakeTimers();
@@ -20,7 +19,9 @@ vi.spyOn(document, 'createElement').mockImplementation(() => {
2019
return mockScriptElement as unknown as HTMLScriptElement;
2120
});
2221

23-
vi.spyOn(document.head, 'appendChild').mockImplementation(() => {});
22+
vi.spyOn(document.head, 'appendChild').mockImplementation(
23+
() => mockScriptElement as any,
24+
);
2425

2526
const mockGlobalFetch = (mockData) => {
2627
const mockFetch = vi.fn().mockResolvedValueOnce(mockResponse(200, mockData));
@@ -65,7 +66,7 @@ describe('fetchWithRetry', () => {
6566
const mockData = { success: true };
6667
mockGlobalFetch(mockData);
6768
const response = await fetchWithRetry({
68-
url: 'https://example.com',
69+
manifestUrl: 'https://example.com',
6970
retryDelay: 0,
7071
});
7172
expect(await response.json()).toEqual(mockData);
@@ -76,7 +77,7 @@ describe('fetchWithRetry', () => {
7677
mockGlobalFetch(mockData);
7778

7879
const response = await fetchWithRetry({
79-
url: 'https://example.com',
80+
manifestUrl: 'https://example.com',
8081
retryDelay: 0,
8182
});
8283

@@ -88,7 +89,7 @@ describe('fetchWithRetry', () => {
8889
mockErrorFetch();
8990
const retryTimes = 3;
9091
const responsePromise = fetchWithRetry({
91-
url: 'https://example.com',
92+
manifestUrl: 'https://example.com',
9293
retryTimes,
9394
retryDelay: 0,
9495
});
@@ -97,14 +98,14 @@ describe('fetchWithRetry', () => {
9798
await expect(responsePromise).rejects.toThrow(
9899
'The request failed three times and has now been abandoned',
99100
);
100-
expect(fetch).toHaveBeenCalledTimes(4); //first fetch + retryTimes fetch
101+
expect(fetch).toHaveBeenCalledTimes(4);
101102
});
102103

103104
it('should fall back to the fallback URL after retries fail', async () => {
104105
mockErrorFetch();
105106
const retryTimes = 3;
106107
const responsePromise = fetchWithRetry({
107-
url: 'https://example.com',
108+
manifestUrl: 'https://example.com',
108109
retryTimes,
109110
retryDelay: 0,
110111
fallback: () => 'https://fallback.com',
@@ -114,15 +115,15 @@ describe('fetchWithRetry', () => {
114115
await expect(responsePromise).rejects.toThrow(
115116
'The request failed three times and has now been abandoned',
116117
);
117-
expect(fetch).toHaveBeenCalledTimes(5); //first fetch + retryTimes fetch
118+
expect(fetch).toHaveBeenCalledTimes(5);
118119
expect(fetch).toHaveBeenLastCalledWith('https://fallback.com', {});
119120
});
120121

121122
it('should build fallback URL from remote after retries fail', async () => {
122123
mockErrorFetch();
123124
const retryTimes = 3;
124125
const responsePromise = fetchWithRetry({
125-
url: 'https://example.com',
126+
manifestUrl: 'https://example.com',
126127
retryTimes,
127128
retryDelay: 0,
128129
fallback: (url) => `${url}/fallback`,
@@ -132,7 +133,7 @@ describe('fetchWithRetry', () => {
132133
await expect(responsePromise).rejects.toThrow(
133134
'The request failed three times and has now been abandoned',
134135
);
135-
expect(fetch).toHaveBeenCalledTimes(5); //first fetch + retryTimes fetch
136+
expect(fetch).toHaveBeenCalledTimes(5);
136137
expect(fetch).toHaveBeenLastCalledWith('https://example.com/fallback', {});
137138
});
138139

@@ -148,28 +149,180 @@ describe('fetchWithRetry', () => {
148149
global.fetch = mockFetch;
149150
await expect(
150151
fetchWithRetry({
151-
url: 'https://example.com',
152+
manifestUrl: 'https://example.com',
152153
retryTimes: 0,
153154
retryDelay: 0,
154155
}),
155156
).rejects.toThrow('Json parse error');
156157
});
157158

158-
it('should build fallback URL from remote after retries fail', async () => {
159-
mockErrorFetch();
160-
const retryTimes = 3;
161-
const responsePromise = fetchWithRetry({
162-
url: 'https://example.com',
163-
retryTimes,
164-
retryDelay: 0,
165-
fallback: (url) => `${url}/fallback`,
159+
describe('getRetryPath functionality', () => {
160+
it('should use original URL for first attempt and getRetryPath for retries', async () => {
161+
const mockFetch = vi
162+
.fn()
163+
.mockRejectedValueOnce(new Error('Network error'))
164+
.mockResolvedValueOnce(mockResponse(200, { success: true }));
165+
166+
global.fetch = mockFetch;
167+
const getRetryPath = vi.fn().mockReturnValue('https://retry.example.com');
168+
169+
const response = await fetchWithRetry({
170+
manifestUrl: 'https://example.com',
171+
retryTimes: 3,
172+
retryDelay: 0,
173+
getRetryPath,
174+
});
175+
176+
expect(fetch).toHaveBeenCalledTimes(2);
177+
expect(fetch).toHaveBeenNthCalledWith(1, 'https://example.com', {});
178+
expect(fetch).toHaveBeenNthCalledWith(2, 'https://retry.example.com', {});
179+
expect(getRetryPath).toHaveBeenCalledWith('https://example.com');
166180
});
167-
vi.advanceTimersByTime(2000 * retryTimes);
168181

169-
await expect(responsePromise).rejects.toThrow(
170-
'The request failed three times and has now been abandoned',
171-
);
172-
expect(fetch).toHaveBeenCalledTimes(5); //first fetch + retryTimes fetch
173-
expect(fetch).toHaveBeenLastCalledWith('https://example.com/fallback', {});
182+
it('should not call getRetryPath on first attempt', async () => {
183+
const mockData = { success: true };
184+
mockGlobalFetch(mockData);
185+
const getRetryPath = vi.fn().mockReturnValue('https://retry.example.com');
186+
const response = await fetchWithRetry({
187+
manifestUrl: 'https://example.com',
188+
retryDelay: 0,
189+
getRetryPath,
190+
});
191+
192+
expect(fetch).toHaveBeenCalledTimes(1);
193+
expect(fetch).toHaveBeenCalledWith('https://example.com', {});
194+
expect(getRetryPath).not.toHaveBeenCalled();
195+
});
196+
197+
it('should use getRetryPath for all retry attempts', async () => {
198+
const mockFetch = vi
199+
.fn()
200+
.mockRejectedValueOnce(new Error('Network error'))
201+
.mockRejectedValueOnce(new Error('Network error'))
202+
.mockRejectedValueOnce(new Error('Network error'))
203+
.mockResolvedValueOnce(mockResponse(200, { success: true }));
204+
205+
global.fetch = mockFetch;
206+
const getRetryPath = vi.fn().mockReturnValue('https://retry.example.com');
207+
const response = await fetchWithRetry({
208+
manifestUrl: 'https://example.com',
209+
retryTimes: 3,
210+
retryDelay: 0,
211+
getRetryPath,
212+
});
213+
214+
expect(fetch).toHaveBeenCalledTimes(4);
215+
expect(fetch).toHaveBeenNthCalledWith(1, 'https://example.com', {});
216+
expect(fetch).toHaveBeenNthCalledWith(2, 'https://retry.example.com', {});
217+
expect(fetch).toHaveBeenNthCalledWith(3, 'https://retry.example.com', {});
218+
expect(fetch).toHaveBeenNthCalledWith(4, 'https://retry.example.com', {});
219+
expect(getRetryPath).toHaveBeenCalledTimes(3);
220+
});
221+
222+
it('should handle getRetryPath returning different URLs for each retry', async () => {
223+
const mockFetch = vi
224+
.fn()
225+
.mockRejectedValueOnce(new Error('Network error'))
226+
.mockRejectedValueOnce(new Error('Network error'))
227+
.mockResolvedValueOnce(mockResponse(200, { success: true }));
228+
229+
global.fetch = mockFetch;
230+
231+
const getRetryPath = vi
232+
.fn()
233+
.mockReturnValueOnce('https://retry1.example.com')
234+
.mockReturnValueOnce('https://retry2.example.com');
235+
236+
const response = await fetchWithRetry({
237+
manifestUrl: 'https://example.com',
238+
retryTimes: 2,
239+
retryDelay: 0,
240+
getRetryPath,
241+
});
242+
243+
expect(fetch).toHaveBeenCalledTimes(3);
244+
expect(fetch).toHaveBeenNthCalledWith(1, 'https://example.com', {});
245+
expect(fetch).toHaveBeenNthCalledWith(
246+
2,
247+
'https://retry1.example.com',
248+
{},
249+
);
250+
expect(fetch).toHaveBeenNthCalledWith(
251+
3,
252+
'https://retry2.example.com',
253+
{},
254+
);
255+
expect(getRetryPath).toHaveBeenCalledTimes(2);
256+
expect(getRetryPath).toHaveBeenNthCalledWith(1, 'https://example.com');
257+
expect(getRetryPath).toHaveBeenNthCalledWith(2, 'https://example.com');
258+
});
259+
260+
it('should handle getRetryPath returning undefined or null', async () => {
261+
const mockFetch = vi
262+
.fn()
263+
.mockRejectedValueOnce(new Error('Network error'))
264+
.mockResolvedValueOnce(mockResponse(200, { success: true }));
265+
266+
global.fetch = mockFetch;
267+
const getRetryPath = vi.fn().mockReturnValue(undefined);
268+
const response = await fetchWithRetry({
269+
manifestUrl: 'https://example.com',
270+
retryTimes: 1,
271+
retryDelay: 0,
272+
getRetryPath,
273+
});
274+
275+
expect(fetch).toHaveBeenCalledTimes(2);
276+
expect(fetch).toHaveBeenNthCalledWith(1, 'https://example.com', {});
277+
expect(fetch).toHaveBeenNthCalledWith(2, 'https://example.com', {});
278+
expect(getRetryPath).toHaveBeenCalledWith('https://example.com');
279+
});
280+
});
281+
282+
describe('retry count and timing', () => {
283+
it('should execute exactly retryTimes + 1 attempts when retryTimes = 3', async () => {
284+
const mockFetch = vi
285+
.fn()
286+
.mockRejectedValueOnce(new Error('Network error'))
287+
.mockRejectedValueOnce(new Error('Network error'))
288+
.mockRejectedValueOnce(new Error('Network error'))
289+
.mockRejectedValueOnce(new Error('Network error'));
290+
291+
global.fetch = mockFetch;
292+
293+
const responsePromise = fetchWithRetry({
294+
manifestUrl: 'https://example.com',
295+
retryTimes: 3,
296+
retryDelay: 0,
297+
});
298+
299+
vi.advanceTimersByTime(1000);
300+
301+
await expect(responsePromise).rejects.toThrow(
302+
'The request failed three times and has now been abandoned',
303+
);
304+
expect(fetch).toHaveBeenCalledTimes(4);
305+
});
306+
307+
it.skip('should respect retryDelay timing', async () => {
308+
const mockFetch = vi
309+
.fn()
310+
.mockRejectedValueOnce(new Error('Network error'))
311+
.mockResolvedValueOnce(mockResponse(200, { success: true }));
312+
313+
global.fetch = mockFetch;
314+
315+
const response = await fetchWithRetry({
316+
manifestUrl: 'https://example.com',
317+
retryTimes: 1,
318+
retryDelay: 100,
319+
});
320+
321+
expect(fetch).toHaveBeenCalledTimes(2);
322+
expect(fetch).toHaveBeenNthCalledWith(1, 'https://example.com', {});
323+
expect(fetch).toHaveBeenNthCalledWith(2, 'https://example.com', {});
324+
325+
expect(response).toBeDefined();
326+
}, 10000);
174327
});
175328
});

0 commit comments

Comments
 (0)