0
# Request Retry Logic
1
2
Configurable retry mechanism for handling transient network failures and server errors with exponential backoff and intelligent retry conditions.
3
4
## Capabilities
5
6
### Retry Configuration
7
8
Configure automatic request retries with detailed control over retry behavior and conditions.
9
10
```javascript { .api }
11
/**
12
* Retry configuration options
13
*/
14
interface RetryOptions {
15
retries?: number; // Number of retry attempts (default: 0)
16
factor?: number; // Exponential backoff factor (default: 2)
17
minTimeout?: number; // Minimum retry timeout in ms (default: 1000)
18
maxTimeout?: number; // Maximum retry timeout in ms (default: Infinity)
19
randomize?: boolean; // Whether to randomize retry intervals (default: false)
20
}
21
22
/**
23
* Callback function called on each retry attempt
24
* @param {Error|Response} cause - Error or response that caused the retry
25
*/
26
type OnRetryCallback = (cause: Error | Response) => void;
27
```
28
29
**Usage Examples:**
30
31
```javascript
32
const fetch = require('make-fetch-happen');
33
34
// Basic retry configuration
35
const response = await fetch('https://unreliable-api.example.com/data', {
36
retry: {
37
retries: 3,
38
factor: 2,
39
minTimeout: 1000,
40
maxTimeout: 10000,
41
randomize: true
42
}
43
});
44
45
// Simple retry count
46
const response2 = await fetch('https://api.example.com/data', {
47
retry: 5 // Equivalent to { retries: 5 }
48
});
49
50
// Disable retries
51
const response3 = await fetch('https://api.example.com/data', {
52
retry: false // Equivalent to { retries: 0 }
53
});
54
55
// With retry callback
56
const response4 = await fetch('https://flaky-api.example.com/data', {
57
retry: { retries: 3 },
58
onRetry: (cause) => {
59
if (cause instanceof Response) {
60
console.log(`Retrying due to ${cause.status} ${cause.statusText}`);
61
} else {
62
console.log(`Retrying due to error: ${cause.message}`);
63
}
64
}
65
});
66
```
67
68
### Retry Conditions
69
70
Requests are automatically retried under specific conditions:
71
72
```javascript { .api }
73
/**
74
* Retry conditions (all must be true):
75
* 1. Request method is NOT 'POST' (to avoid duplicate mutations)
76
* 2. Request does not have a streaming body
77
* 3. One of the following:
78
* a. Response status is 408, 420, 429, or >= 500
79
* b. Request failed with retriable error codes
80
* c. Request failed with retriable error types
81
*/
82
83
/**
84
* Retriable error codes:
85
*/
86
const RETRY_ERRORS = [
87
'ECONNRESET', // Remote socket closed
88
'ECONNREFUSED', // Connection refused
89
'EADDRINUSE', // Address in use
90
'ETIMEDOUT', // Operation timed out
91
'ECONNECTIONTIMEOUT', // Connection timeout (from @npmcli/agent)
92
'EIDLETIMEOUT', // Idle timeout (from @npmcli/agent)
93
'ERESPONSETIMEOUT', // Response timeout (from @npmcli/agent)
94
'ETRANSFERTIMEOUT' // Transfer timeout (from @npmcli/agent)
95
];
96
97
/**
98
* Retriable error types:
99
*/
100
const RETRY_TYPES = [
101
'request-timeout' // Request timeout from fetch
102
];
103
104
/**
105
* Non-retriable conditions:
106
* - ENOTFOUND (DNS resolution failure - likely offline or bad hostname)
107
* - EINVALIDPROXY (Invalid proxy configuration)
108
* - EINVALIDRESPONSE (Invalid response from server)
109
* - All 2xx and 3xx response codes
110
* - 4xx response codes except 408, 420, 429
111
*/
112
```
113
114
### Exponential Backoff
115
116
Retry timing follows exponential backoff with optional randomization:
117
118
```javascript { .api }
119
/**
120
* Backoff calculation:
121
* timeout = min(minTimeout * (factor ^ attempt), maxTimeout)
122
*
123
* If randomize is true:
124
* timeout = timeout * (1 + Math.random())
125
*
126
* Default values:
127
* - minTimeout: 1000ms
128
* - factor: 2
129
* - maxTimeout: Infinity
130
* - randomize: false
131
*/
132
```
133
134
**Usage Examples:**
135
136
```javascript
137
// Custom backoff strategy
138
const response = await fetch('https://api.example.com/data', {
139
retry: {
140
retries: 5,
141
factor: 1.5, // Slower exponential growth
142
minTimeout: 2000, // Start with 2 second delay
143
maxTimeout: 30000, // Cap at 30 seconds
144
randomize: true // Add jitter to prevent thundering herd
145
}
146
});
147
148
// Backoff progression with above config:
149
// Attempt 1: 2000-4000ms (randomized)
150
// Attempt 2: 3000-6000ms (randomized)
151
// Attempt 3: 4500-9000ms (randomized)
152
// Attempt 4: 6750-13500ms (randomized)
153
// Attempt 5: 10125-20250ms (randomized, but capped at 30000ms)
154
```
155
156
### Retry Callbacks
157
158
Monitor retry attempts and implement custom logic:
159
160
```javascript { .api }
161
/**
162
* Retry callback receives the cause of the retry
163
* @param {Error|Response} cause - What triggered the retry
164
*/
165
type OnRetryCallback = (cause: Error | Response) => void;
166
```
167
168
**Usage Examples:**
169
170
```javascript
171
let retryCount = 0;
172
173
const response = await fetch('https://api.example.com/data', {
174
retry: { retries: 3 },
175
onRetry: (cause) => {
176
retryCount++;
177
178
if (cause instanceof Response) {
179
console.log(`Retry ${retryCount}: Server returned ${cause.status}`);
180
181
// Log response headers for debugging
182
const rateLimitReset = cause.headers.get('x-rate-limit-reset');
183
if (rateLimitReset) {
184
console.log(`Rate limit resets at: ${new Date(rateLimitReset * 1000)}`);
185
}
186
} else {
187
console.log(`Retry ${retryCount}: Network error - ${cause.message}`);
188
console.log(`Error code: ${cause.code}`);
189
}
190
}
191
});
192
```
193
194
### POST Request Handling
195
196
POST requests are handled specially to prevent accidental duplicate operations:
197
198
```javascript { .api }
199
/**
200
* POST request retry behavior:
201
* - POST requests are never retried for response status codes
202
* - POST requests are only retried for network-level errors
203
* - This prevents accidental duplicate mutations on the server
204
*/
205
```
206
207
**Usage Examples:**
208
209
```javascript
210
// GET request - will retry on 500 errors
211
const getData = await fetch('https://api.example.com/data', {
212
retry: { retries: 3 }
213
});
214
215
// POST request - will only retry on network errors, not 500 responses
216
const postData = await fetch('https://api.example.com/data', {
217
method: 'POST',
218
body: JSON.stringify({ action: 'create' }),
219
headers: { 'Content-Type': 'application/json' },
220
retry: { retries: 3 } // Only retries network errors for POST
221
});
222
223
// Use idempotent POST patterns for retries
224
const idempotentPost = await fetch('https://api.example.com/data', {
225
method: 'POST',
226
body: JSON.stringify({
227
idempotencyKey: 'unique-key-123',
228
action: 'create'
229
}),
230
headers: {
231
'Content-Type': 'application/json',
232
'Idempotency-Key': 'unique-key-123'
233
},
234
retry: { retries: 3 }
235
});
236
```
237
238
### Advanced Retry Patterns
239
240
Common patterns for robust retry handling:
241
242
```javascript
243
// Circuit breaker pattern
244
class CircuitBreaker {
245
constructor(threshold = 5, timeout = 60000) {
246
this.failureCount = 0;
247
this.lastFailTime = 0;
248
this.threshold = threshold;
249
this.timeout = timeout;
250
}
251
252
async call(fetchFn) {
253
if (this.isOpen()) {
254
throw new Error('Circuit breaker is open');
255
}
256
257
try {
258
const result = await fetchFn();
259
this.onSuccess();
260
return result;
261
} catch (error) {
262
this.onFailure();
263
throw error;
264
}
265
}
266
267
isOpen() {
268
return this.failureCount >= this.threshold &&
269
(Date.now() - this.lastFailTime) < this.timeout;
270
}
271
272
onSuccess() {
273
this.failureCount = 0;
274
}
275
276
onFailure() {
277
this.failureCount++;
278
this.lastFailTime = Date.now();
279
}
280
}
281
282
// Usage with circuit breaker
283
const breaker = new CircuitBreaker();
284
const response = await breaker.call(() =>
285
fetch('https://api.example.com/data', {
286
retry: { retries: 2 }
287
})
288
);
289
290
// Conditional retry based on error type
291
const smartRetry = {
292
retries: 3,
293
onRetry: (cause) => {
294
if (cause instanceof Response && cause.status === 429) {
295
// For rate limiting, wait longer
296
const retryAfter = cause.headers.get('retry-after');
297
if (retryAfter) {
298
console.log(`Rate limited, retry after ${retryAfter} seconds`);
299
}
300
}
301
}
302
};
303
304
// Metrics collection
305
const metrics = { attempts: 0, retries: 0, failures: 0 };
306
307
const response = await fetch('https://api.example.com/data', {
308
retry: { retries: 3 },
309
onRetry: () => {
310
metrics.retries++;
311
}
312
});
313
314
metrics.attempts = 1 + metrics.retries;
315
if (!response.ok) {
316
metrics.failures++;
317
}
318
```