Skip to content

Commit 6ed0634

Browse files
authored
Handle GitHub API rate limits in script (#2471)
1 parent 6dbb292 commit 6ed0634

File tree

1 file changed

+51
-3
lines changed

1 file changed

+51
-3
lines changed

docs/scripts/update-contributors.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from "node:fs/promises";
2+
import timers from "node:timers/promises";
23
import { URL } from "node:url";
34

45
const MAINTAINERS = {
@@ -221,16 +222,61 @@ class UserFetchError extends Error {
221222
}
222223
}
223224

224-
async function fetchUserInfo(username) {
225-
const res = await fetch(`https://api.github.com/users/${username}`, {
225+
const BACKOFF_INTERVALS_MINUTES = [1, 2, 3, 5];
226+
const MAX_RETRIES = BACKOFF_INTERVALS_MINUTES.length - 1;
227+
228+
async function rateLimitDelay({ retryAfter, ratelimitReset, retryCount }) {
229+
let timeoutInMilliseconds = BACKOFF_INTERVALS_MINUTES[retryCount] * 1000;
230+
if (retryAfter) {
231+
timeoutInMilliseconds = retryAfter * 1000;
232+
} else if (ratelimitRemaining === "0" && ratelimitReset) {
233+
timeoutInMilliseconds = ratelimitReset * 1000 - Date.now();
234+
}
235+
236+
const timeoutInSeconds = (timeoutInMilliseconds / 1000).toLocaleString();
237+
console.warn(`Waiting for ${timeoutInSeconds} seconds...`);
238+
239+
await timers.setTimeout(timeoutInMilliseconds);
240+
}
241+
242+
async function fetchUserInfo(username, retryCount = 0) {
243+
if (retryCount >= MAX_RETRIES) {
244+
throw new Error(`Hit max retries (${MAX_RETRIES}) for fetching user ${username}`);
245+
}
246+
247+
const request = new Request(`https://api.github.com/users/${username}`, {
226248
headers: {
227249
Accept: "application/vnd.github+json",
228250
"X-GitHub-Api-Version": "2022-11-28",
229251
},
230252
});
253+
if (process.env.GITHUB_TOKEN) {
254+
request.headers.set("Authorization", `Bearer ${process.env.GITHUB_TOKEN}`);
255+
}
256+
257+
const res = await fetch(request);
258+
231259
if (!res.ok) {
260+
const retryAfter = res.headers.get("retry-after"); // seconds
261+
const ratelimitRemaining = res.headers.get("x-ratelimit-remaining"); // quantity of requests remaining
262+
const ratelimitReset = res.headers.get("x-ratelimit-reset"); // UTC epoch seconds
263+
264+
if (retryAfter || ratelimitRemaining === "0" || ratelimitReset) {
265+
// See https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately
266+
console.warn("Rate limited by GitHub API");
267+
268+
await rateLimitDelay({
269+
retryAfter,
270+
ratelimitReset,
271+
retryCount,
272+
});
273+
274+
return await fetchUserInfo(username, retryCount + 1);
275+
}
276+
232277
throw new UserFetchError(`${res.url} responded with ${res.status}`, res);
233278
}
279+
234280
return await res.json();
235281
}
236282

@@ -283,4 +329,6 @@ async function main() {
283329
}
284330
}
285331

286-
main();
332+
if (import.meta.main) {
333+
main();
334+
}

0 commit comments

Comments
 (0)