From 413ca3769232d1d178cf7e15f4652a836d6e6ed9 Mon Sep 17 00:00:00 2001 From: Hunter Tunnicliff Date: Thu, 2 Oct 2025 08:50:59 -0700 Subject: [PATCH 1/4] Handle GitHub API rate limits in script --- docs/scripts/update-contributors.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index 7222ffadd..da538fb26 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -1,4 +1,5 @@ import fs from "node:fs/promises"; +import timers from "node:timers/promises"; import { URL } from "node:url"; const MAINTAINERS = { @@ -227,9 +228,33 @@ async function fetchUserInfo(username) { "X-GitHub-Api-Version": "2022-11-28", }, }); + if (!res.ok) { + const retryAfter = res.headers.get("retry-after"); // seconds + const ratelimitRemaining = res.headers.get("x-ratelimit-remaining"); // quantity of requests remaining + const ratelimitReset = res.headers.get("x-ratelimit-reset"); // UTC epoch seconds + + if (retryAfter || ratelimitRemaining === "0" || ratelimitReset) { + // 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 + console.warn("Rate limited by GitHub API"); + + let timeoutInMilliseconds = 60 * 1000; // default to 1 minute + if (retryAfter) { + timeoutInMilliseconds = retryAfter * 1000; + } else if (ratelimitRemaining === "0" && ratelimitReset) { + timeoutInMilliseconds = ratelimitReset * 1000 - Date.now(); + } + + console.warn(`Waiting for ${timeoutInMilliseconds / 1000} seconds...`); + + await timers.setTimeout(timeoutInMilliseconds); + + return await fetchUserInfo(username); + } + throw new UserFetchError(`${res.url} responded with ${res.status}`, res); } + return await res.json(); } @@ -282,4 +307,6 @@ async function main() { } } -main(); +if (import.meta.main) { + main(); +} From cb80a5db22e8e566ae1692b8ef064a0ba731c13e Mon Sep 17 00:00:00 2001 From: Hunter Tunnicliff Date: Fri, 3 Oct 2025 09:49:10 -0700 Subject: [PATCH 2/4] Add maximum retry limit --- docs/scripts/update-contributors.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index da538fb26..3c2616341 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -221,7 +221,13 @@ class UserFetchError extends Error { } } -async function fetchUserInfo(username) { +const MAX_RETRIES = 5; + +async function fetchUserInfo(username, retryCount = 0) { + if (retryCount >= MAX_RETRIES) { + throw new Error(`Hit max retries (${MAX_RETRIES}) for fetching user ${username}`); + } + const res = await fetch(`https://api.github.com/users/${username}`, { headers: { Accept: "application/vnd.github+json", @@ -249,7 +255,7 @@ async function fetchUserInfo(username) { await timers.setTimeout(timeoutInMilliseconds); - return await fetchUserInfo(username); + return await fetchUserInfo(username, retryCount + 1); } throw new UserFetchError(`${res.url} responded with ${res.status}`, res); From 039d1038ff2ca30743d7171c1a4a8fec4a05b2b1 Mon Sep 17 00:00:00 2001 From: Hunter Tunnicliff Date: Fri, 3 Oct 2025 09:56:16 -0700 Subject: [PATCH 3/4] Add retry backoff strategy --- docs/scripts/update-contributors.js | 32 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index 3c2616341..c8b17f9a1 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -221,7 +221,22 @@ class UserFetchError extends Error { } } -const MAX_RETRIES = 5; +const BACKOFF_INTERVALS_MINUTES = [1, 2, 3, 5]; +const MAX_RETRIES = BACKOFF_INTERVALS_MINUTES.length - 1; + +async function rateLimitDelay({ retryAfter, ratelimitReset, retryCount }) { + let timeoutInMilliseconds = BACKOFF_INTERVALS_MINUTES[retryCount] * 1000; + if (retryAfter) { + timeoutInMilliseconds = retryAfter * 1000; + } else if (ratelimitRemaining === "0" && ratelimitReset) { + timeoutInMilliseconds = ratelimitReset * 1000 - Date.now(); + } + + const timeoutInSeconds = (timeoutInMilliseconds / 1000).toLocaleString(); + console.warn(`Waiting for ${timeoutInSeconds} seconds...`); + + await timers.setTimeout(timeoutInMilliseconds); +} async function fetchUserInfo(username, retryCount = 0) { if (retryCount >= MAX_RETRIES) { @@ -244,16 +259,11 @@ async function fetchUserInfo(username, retryCount = 0) { // 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 console.warn("Rate limited by GitHub API"); - let timeoutInMilliseconds = 60 * 1000; // default to 1 minute - if (retryAfter) { - timeoutInMilliseconds = retryAfter * 1000; - } else if (ratelimitRemaining === "0" && ratelimitReset) { - timeoutInMilliseconds = ratelimitReset * 1000 - Date.now(); - } - - console.warn(`Waiting for ${timeoutInMilliseconds / 1000} seconds...`); - - await timers.setTimeout(timeoutInMilliseconds); + await rateLimitDelay({ + retryAfter, + ratelimitReset, + retryCount, + }); return await fetchUserInfo(username, retryCount + 1); } From e9e49ce4a7725188c3a5982a19da27b663ad199d Mon Sep 17 00:00:00 2001 From: Hunter Tunnicliff Date: Fri, 3 Oct 2025 17:27:50 -0700 Subject: [PATCH 4/4] Add github token auth (#2481) --- docs/scripts/update-contributors.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index c8b17f9a1..1836b9fae 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -243,12 +243,17 @@ async function fetchUserInfo(username, retryCount = 0) { throw new Error(`Hit max retries (${MAX_RETRIES}) for fetching user ${username}`); } - const res = await fetch(`https://api.github.com/users/${username}`, { + const request = new Request(`https://api.github.com/users/${username}`, { headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, }); + if (process.env.GITHUB_TOKEN) { + request.headers.set("Authorization", `Bearer ${process.env.GITHUB_TOKEN}`); + } + + const res = await fetch(request); if (!res.ok) { const retryAfter = res.headers.get("retry-after"); // seconds