Skip to content

Commit 912281d

Browse files
authored
BREAKING-RELEASE: change default behavior so fetch-retry retries on FetchErrors of type system; add custom error handling option (#63)
* fetch-retry retries on FetchErrors of type system * network timeout test mocked * fix test * fix function description * make test fuzzy * skip not mocked enotfound test since it is not stable
1 parent 48755df commit 912281d

File tree

3 files changed

+321
-92
lines changed

3 files changed

+321
-92
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Without configuring any parameters, the retry behavior will be as follows:
2424
- retry for 60s
2525
- retry inital delay of 100ms with exponential backoff, configurable as a multiplier defaulting to 2
2626
- retry only on 5xx response
27+
- retry on all FetchError system errors
28+
- see node-fetch error handling: https://github.com/node-fetch/node-fetch/blob/main/docs/ERROR-HANDLING.md
2729
- socket timeout of 30s
2830
```js
2931
const fetch = require('@adobe/node-fetch-retry');
@@ -57,6 +59,7 @@ All the retry options are configurable and can be set in `retryOptions` in the `
5759
| `retryInitialDelay` | Number | time in milliseconds to wait between retries |`NODE_FETCH_RETRY_INITIAL_WAIT` | 100 ms |
5860
| `retryBackoff` | Number | backoff factor for wait time between retries | `NODE_FETCH_RETRY_BACKOFF` | 2.0 |
5961
| `retryOnHttpResponse` | Function | a *function* determining whether to retry given the HTTP response | none | retry on all 5xx errors|
62+
| `retryOnHttpError` | Function | a *function* determining whether to retry given the HTTP error exception thrown | none | retry on all `FetchError`'s of type `system`|
6063
| `socketTimeout` | Number | time until socket timeout in milliseconds. _Note: if `socketTimeout` is >= `retryMaxDuration`, it will automatically adjust the socket timeout to be exactly half of the `retryMaxDuration`. To disable this feature, see `forceSocketTimeout` below_ | `NODE_FETCH_RETRY_SOCKET_TIMEOUT` | 30000 ms |
6164
| `forceSocketTimeout` | Boolean | If true, socket timeout will be forced to use `socketTimeout` property declared regardless of the `retryMaxDuration`. _Note: this feature was designed to help with unit testing and is not intended to be used in practice_ | `NODE_FETCH_RETRY_FORCE_TIMEOUT` | false |
6265

@@ -129,6 +132,20 @@ async main() {
129132
});
130133
}
131134
```
135+
This example shows how to retry on all HTTP errors thrown as an exception:
136+
```js
137+
const fetch = require('@adobe/node-fetch-retry');
138+
139+
async main() {
140+
const response = await fetch(url, {
141+
retryOptions: {
142+
retryOnHttpError: function (error) {
143+
return true;
144+
}
145+
}
146+
});
147+
}
148+
```
132149

133150

134151
### Disable Retry

index.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function isResponseTimedOut(retryOptions) {
4545
function shouldRetry(retryOptions, error, response, waitTime) {
4646
if (getTimeRemaining(retryOptions) < waitTime) {
4747
return false;
48+
} else if (retryOptions && retryOptions.retryOnHttpError && error != null) {
49+
return retryOptions.retryOnHttpError(error);
4850
} else if (retryOptions && retryOptions.retryOnHttpResponse) {
4951
return retryOptions.retryOnHttpResponse(response);
5052
} else {
@@ -92,6 +94,8 @@ function retryInit(options={}) {
9294
retryBackoff: retryOptions.retryBackoff || DEFAULT_BACKOFF,
9395
retryOnHttpResponse: ((typeof retryOptions.retryOnHttpResponse === 'function') && retryOptions.retryOnHttpResponse) ||
9496
((response) => { return response.status >= 500; }),
97+
retryOnHttpError: ((typeof retryOptions.retryOnHttpError === 'function') && retryOptions.retryOnHttpError) ||
98+
((error) => { return shouldRetryOnHttpError(error); }),
9599
socketTimeout: socketTimeoutValue
96100
};
97101
}
@@ -125,6 +129,9 @@ function checkParameters(retryOptions) {
125129
if (retryOptions.retryOnHttpResponse && !(typeof retryOptions.retryOnHttpResponse === 'function')) {
126130
throw new Error(`'retryOnHttpResponse' must be a function: ${retryOptions.retryOnHttpResponse}`);
127131
}
132+
if (retryOptions.retryOnHttpError && !(typeof retryOptions.retryOnHttpError === 'function')) {
133+
throw new Error(`'retryOnHttpError' must be a function: ${retryOptions.retryOnHttpError}`);
134+
}
128135
if (typeof retryOptions.retryBackoff !== 'undefined'
129136
&& !(Number.isInteger(retryOptions.retryBackoff) && retryOptions.retryBackoff >= 1.0)) {
130137
throw new Error('`retryBackoff` must be a positive integer >= 1');
@@ -135,24 +142,26 @@ function checkParameters(retryOptions) {
135142
}
136143

137144
/**
138-
* Format HTTP error response into response object
145+
* Evaluates whether or not to retry based on HTTP error
139146
* @param {Object} error
140-
* @returns
147+
* @returns Returns true for all FetchError's of type `system`
141148
*/
142-
function getResponseFromHttpError(error) {
143-
let code = error.code;
144-
if (!code || typeof(code) !== 'number') {
145-
console.warn(`error code is invalid ${code}. Setting status code to 500`);
146-
code = 500;
149+
function shouldRetryOnHttpError(error) {
150+
// special handling for known fetch errors: https://github.com/node-fetch/node-fetch/blob/main/docs/ERROR-HANDLING.md
151+
// retry on all errors originating from Node.js core
152+
if (error.name === 'FetchError' && error.type === 'system') {
153+
console.error(`FetchError failed with code: ${error.code}; message: ${error.message}`);
154+
return true;
147155
}
148-
return {status: code, statusMessage : error.message || "Unknown server error"};
156+
return false;
149157
}
150158

151159
/**
152160
* @typedef {Object} RetryOptions options for retry or false if want to disable retry
153161
* @property {Integer} retryMaxDuration time (in milliseconds) to retry until throwing an error
154162
* @property {Integer} retryInitialDelay time to wait between retries in milliseconds
155163
* @property {Function} retryOnHttpResponse a function determining whether to retry on a specific HTTP code
164+
* @property {Function} retryOnHttpError a function determining whether to retry on a specific HTTP error
156165
* @property {Integer} retryBackoff backoff factor for wait time between retries (defaults to 2.0)
157166
* @property {Integer} socketTimeout Optional socket timeout in milliseconds (defaults to 60000ms)
158167
* @property {Boolean} forceSocketTimeout If true, socket timeout will be forced to use `socketTimeout` property declared (defaults to false)
@@ -162,6 +171,11 @@ function getResponseFromHttpError(error) {
162171
* @property {Number} response response from the http fetch call
163172
* @returns {Boolean} true if want to retry on this response, false if do not want to retry on the response
164173
*/
174+
/**
175+
* @typedef {Function} retryOnHttpError determines whether to do a retry on the HTTP error response
176+
* @property {Object} error error thrown during the fetch request
177+
* @returns {Boolean} true if want to retry on this error, false if do not want to retry on the response
178+
*/
165179
/**
166180
* @typedef {Object} Options options for fetch-retry
167181
* @property {Object} RetryOptions options for retry or false if want to disable retry
@@ -203,15 +217,14 @@ module.exports = async function (url, options) {
203217
return resolve(response);
204218
}
205219
} catch (error) {
206-
const response = getResponseFromHttpError(error);
207-
if (!shouldRetry(retryOptions, error, response, waitTime)) {
220+
if (!shouldRetry(retryOptions, error, null, waitTime)) {
208221
if (error.name === 'AbortError') {
209222
return reject(new FetchError(`network timeout at ${url}`, 'request-timeout'));
210223
} else {
211224
return reject(error);
212225
}
213226
}
214-
console.error(`Retrying in ${waitTime} milliseconds, attempt ${attempt} error: ${error.message}`);
227+
console.error(`Retrying in ${waitTime} milliseconds, attempt ${attempt} error: ${error.name}, ${error.message}`);
215228
} finally {
216229
clearTimeout(timeoutHandler);
217230
}

0 commit comments

Comments
 (0)