Skip to content

Commit c8b9800

Browse files
committed
Add automatic retry policy
1 parent edeed24 commit c8b9800

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const ApiError = require('./lib/error');
2+
const { withAutomaticRetries } = require('./lib/util');
23

34
const collections = require('./lib/collections');
45
const models = require('./lib/models');
@@ -201,7 +202,7 @@ class Replicate {
201202
body: data ? JSON.stringify(data) : undefined,
202203
};
203204

204-
const response = await this.fetch(url, init);
205+
const response = await withAutomaticRetries(async () => this.fetch(url, init));
205206

206207
if (!response.ok) {
207208
const request = new Request(url, init);

lib/util.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const ApiError = require('./error');
2+
3+
/**
4+
* Automatically retry a request if it fails with an appropriate status code.
5+
*
6+
* A GET request is retried if it fails with a 429 or 5xx status code.
7+
* A non-GET request is retried only if it fails with a 429 status code.
8+
*
9+
* If the response sets a Retry-After header,
10+
* the request is retried after the number of seconds specified in the header.
11+
* Otherwise, the request is retried after the specified interval,
12+
* with exponential backoff and jitter.
13+
*
14+
* @param {Function} request - A function that returns a Promise that resolves with a Response object
15+
* @param {object} options
16+
* @param {number} [options.retries] - Number of retries. Defaults to 5
17+
* @param {number} [options.interval] - Interval between retries in milliseconds. Defaults to 500
18+
* @returns {Promise<Response>} - Resolves with the response object
19+
* @throws {ApiError} If the request failed
20+
*/
21+
async function withAutomaticRetries(request, options = {}) {
22+
const retries = options.retries || 5;
23+
const interval = options.interval || 500;
24+
const jitter = options.jitter || 100;
25+
26+
const shouldRetry = (response) => {
27+
if (request.method === 'GET') {
28+
return response.status === 429 || response.status >= 500;
29+
}
30+
31+
return response.status === 429;
32+
};
33+
34+
// eslint-disable-next-line no-promise-executor-return
35+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
36+
37+
let attempts = 0;
38+
do {
39+
let delay = (interval * (2 ** attempts)) + (Math.random() * jitter);
40+
41+
/* eslint-disable no-await-in-loop */
42+
try {
43+
const response = await request();
44+
if (response.ok || !shouldRetry(response)) {
45+
return response;
46+
}
47+
} catch (error) {
48+
if (error instanceof ApiError) {
49+
const retryAfter = error.response.headers.get('Retry-After');
50+
if (retryAfter) {
51+
if (!Number.isInteger(retryAfter)) { // Retry-After is a date
52+
const date = new Date(retryAfter);
53+
if (!Number.isNaN(date.getTime())) {
54+
delay = date.getTime() - new Date().getTime();
55+
}
56+
} else { // Retry-After is a number of seconds
57+
delay = retryAfter * 1000;
58+
}
59+
}
60+
}
61+
}
62+
63+
if (Number.isInteger(retries) && retries > 0) {
64+
if (Number.isInteger(delay) && delay > 0) {
65+
await sleep(interval * 2 ** (options.retries - retries));
66+
}
67+
attempts += 1;
68+
}
69+
/* eslint-enable no-await-in-loop */
70+
} while (attempts < retries);
71+
72+
return request();
73+
}
74+
75+
module.exports = { withAutomaticRetries };

0 commit comments

Comments
 (0)