Skip to content

Commit 0960f34

Browse files
feat: Smarter retries and better error feedback
- Modified `fetchWithRetry` to stop retrying on 4xx client errors (excluding 429 Too Many Requests). This prevents unnecessary waiting for errors that will not resolve (e.g., 404 Not Found, 400 Bad Request). - Updated `saveError` to print the specific error message to `stderr` in addition to logging the file path. This gives the user immediate context on why the request failed. - Added comprehensive tests in `tests/commands/unipept/retry_logic.test.ts` to verify the retry behavior across various scenarios (5xx, 404, 400, 429, network error).
1 parent d93485c commit 0960f34

File tree

2 files changed

+117
-3
lines changed

2 files changed

+117
-3
lines changed

lib/commands/unipept/unipept_subcommand.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,10 @@ export abstract class UnipeptSubcommand {
208208
*/
209209
async saveError(message: string) {
210210
const errorPath = this.errorFilePath();
211-
mkdir(path.dirname(errorPath), { recursive: true });
211+
await mkdir(path.dirname(errorPath), { recursive: true });
212212
await appendFile(errorPath, `${message}\n`);
213-
console.error(`API request failed! log can be found in ${errorPath}`);
213+
console.error(`API request failed: ${message}`);
214+
console.error(`Log can be found in ${errorPath}`);
214215
}
215216

216217
/**
@@ -228,7 +229,17 @@ export abstract class UnipeptSubcommand {
228229
}
229230
})
230231
.catch(async error => {
231-
if (retries > 0) {
232+
let shouldRetry = retries > 0;
233+
234+
// check if we should stop retrying based on error message (from the reject above)
235+
if (typeof error === 'string') {
236+
const status = parseInt(error.split(' ')[0]);
237+
if (status >= 400 && status < 500 && status !== 429) {
238+
shouldRetry = false;
239+
}
240+
}
241+
242+
if (shouldRetry) {
232243
// retry with delay
233244
// console.error("retrying");
234245
const delay = 5000 * Math.random();
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { UnipeptSubcommand } from '../../../lib/commands/unipept/unipept_subcommand';
3+
4+
// Concrete implementation for testing abstract class
5+
class TestSubcommand extends UnipeptSubcommand {
6+
constructor() {
7+
super('test');
8+
}
9+
defaultBatchSize(): number { return 10; }
10+
}
11+
12+
describe('UnipeptSubcommand Retry Logic', () => {
13+
let command: TestSubcommand;
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
let fetchSpy: any;
16+
17+
beforeEach(() => {
18+
command = new TestSubcommand();
19+
// @ts-ignore
20+
fetchSpy = vi.spyOn(global, 'fetch');
21+
});
22+
23+
afterEach(() => {
24+
vi.restoreAllMocks();
25+
});
26+
27+
it('should retry on 500 error', async () => {
28+
// Mock fetch to fail with 500 once, then succeed
29+
fetchSpy
30+
.mockResolvedValueOnce({
31+
ok: false,
32+
status: 500,
33+
statusText: 'Internal Server Error'
34+
} as Response)
35+
.mockResolvedValueOnce({
36+
ok: true,
37+
status: 200,
38+
statusText: 'OK'
39+
} as Response);
40+
41+
const result = await command.fetchWithRetry('http://example.com', {}, 3);
42+
expect(result.ok).toBe(true);
43+
expect(fetchSpy).toHaveBeenCalledTimes(2);
44+
});
45+
46+
it('should retry on network error', async () => {
47+
fetchSpy
48+
.mockRejectedValueOnce(new Error('Network error'))
49+
.mockResolvedValueOnce({
50+
ok: true,
51+
status: 200,
52+
statusText: 'OK'
53+
} as Response);
54+
55+
const result = await command.fetchWithRetry('http://example.com', {}, 3);
56+
expect(result.ok).toBe(true);
57+
expect(fetchSpy).toHaveBeenCalledTimes(2);
58+
});
59+
60+
it('should NOT retry on 404 error', async () => {
61+
fetchSpy.mockResolvedValue({
62+
ok: false,
63+
status: 404,
64+
statusText: 'Not Found'
65+
} as Response);
66+
67+
await expect(command.fetchWithRetry('http://example.com', {}, 3))
68+
.rejects.toMatch(/Failed to fetch data from the Unipept API: 404 Not Found/);
69+
70+
expect(fetchSpy).toHaveBeenCalledTimes(1);
71+
});
72+
73+
it('should NOT retry on 400 error', async () => {
74+
fetchSpy.mockResolvedValue({
75+
ok: false,
76+
status: 400,
77+
statusText: 'Bad Request'
78+
} as Response);
79+
80+
await expect(command.fetchWithRetry('http://example.com', {}, 3))
81+
.rejects.toMatch(/Failed to fetch data from the Unipept API: 400 Bad Request/);
82+
83+
expect(fetchSpy).toHaveBeenCalledTimes(1);
84+
});
85+
86+
it('should retry on 429 error', async () => {
87+
fetchSpy
88+
.mockResolvedValueOnce({
89+
ok: false,
90+
status: 429,
91+
statusText: 'Too Many Requests'
92+
} as Response)
93+
.mockResolvedValueOnce({
94+
ok: true,
95+
status: 200,
96+
statusText: 'OK'
97+
} as Response);
98+
99+
const result = await command.fetchWithRetry('http://example.com', {}, 3);
100+
expect(result.ok).toBe(true);
101+
expect(fetchSpy).toHaveBeenCalledTimes(2);
102+
});
103+
});

0 commit comments

Comments
 (0)