Skip to content

Commit 5277cb0

Browse files
committed
feat: Robust GitHub API rate limit detection and graceful exit (#9087)
1 parent 5a3a693 commit 5277cb0

3 files changed

Lines changed: 50 additions & 1 deletion

File tree

apps/devrank/services/GitHub.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,32 @@ class GitHub extends Base {
100100
if (remaining !== null) this.rateLimit.remaining = parseInt(remaining, 10);
101101
if (reset !== null) this.rateLimit.reset = parseInt(reset, 10);
102102
if (limit !== null) this.rateLimit.limit = parseInt(limit, 10);
103+
104+
// Debug: Warn if headers are missing but we are at default (implying no update ever happened)
105+
if (remaining === null && this.rateLimit.remaining === 5000) {
106+
// Only log once or sparsely to avoid spam
107+
if (!this._headerWarned) {
108+
console.warn('[GitHub] Warning: `x-rate-limit-*` headers not found. Falling back to body (if available).');
109+
this._headerWarned = true;
110+
}
111+
}
112+
}
113+
114+
/**
115+
* Updates rate limit from GraphQL body.
116+
* @param {Object} rateLimit
117+
* @private
118+
*/
119+
#updateFromBody(rateLimit) {
120+
if (!rateLimit) return;
121+
122+
if (rateLimit.remaining !== undefined) this.rateLimit.remaining = rateLimit.remaining;
123+
if (rateLimit.limit !== undefined) this.rateLimit.limit = rateLimit.limit;
124+
125+
if (rateLimit.resetAt) {
126+
// GraphQL returns ISO string, we store epoch seconds
127+
this.rateLimit.reset = Math.floor(new Date(rateLimit.resetAt).getTime() / 1000);
128+
}
103129
}
104130

105131
/**
@@ -140,6 +166,11 @@ class GitHub extends Base {
140166

141167
const json = await response.json();
142168

169+
// Hook for body-based rate limit (more reliable for GraphQL)
170+
if (json.data?.rateLimit) {
171+
this.#updateFromBody(json.data.rateLimit);
172+
}
173+
143174
if (json.errors) {
144175
// Sometimes 502s come as 200 OK with errors body
145176
const isGatewayError = json.errors.some(e => e.message?.includes('502') || e.message?.includes('504'));
@@ -197,6 +228,9 @@ class GitHub extends Base {
197228
this.#updateRateLimit(response.headers);
198229

199230
if (!response.ok) {
231+
if (response.status === 403) {
232+
this.rateLimit.remaining = 0;
233+
}
200234
throw new Error(`REST Error: ${response.status} ${response.statusText}`);
201235
}
202236

apps/devrank/services/Spider.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ class Spider extends Base {
514514
.filter(c => c.type === 'User')
515515
.map(c => c.login);
516516
} catch (e) {
517+
// Kill-switch for rate limits
518+
if (e.message.includes('403') || e.message.includes('rate limit')) {
519+
console.warn(`[Spider] 🚨 Rate limit hit scanning ${fullName}. Forcing shutdown.`);
520+
GitHub.rateLimit.remaining = 0;
521+
}
522+
517523
console.error(`[Spider] Failed to fetch contributors for ${fullName}: ${e.message}`);
518524
return [];
519525
}

apps/devrank/services/Updater.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ class Updater extends Base {
8787
}
8888
} catch (error) {
8989
console.log(`[${login}] FAILED: ${error.message}`);
90+
91+
// Kill-switch: If we hit a rate limit error, force internal state to 0 to trigger graceful shutdown
92+
if (error.message.includes('rate limit')) {
93+
console.warn(`[Updater] 🚨 Rate limit hit for ${login}. Forcing shutdown sequence.`);
94+
GitHub.rateLimit.remaining = 0;
95+
}
9096
}
9197
};
9298

@@ -160,6 +166,7 @@ class Updater extends Base {
160166
// 1. Fetch Basic Profile & Social Accounts (GraphQL)
161167
const profileQuery = `
162168
query {
169+
rateLimit { remaining limit resetAt }
163170
user(login: "${username}") {
164171
createdAt
165172
avatarUrl
@@ -227,7 +234,9 @@ class Updater extends Base {
227234
const contribData = {};
228235

229236
const fetchYears = async (fromYear, toYear) => {
230-
let query = `query { user(login: "${username}") {`;
237+
let query = `query {
238+
rateLimit { remaining limit resetAt }
239+
user(login: "${username}") {`;
231240
for (let year = fromYear; year <= toYear; year++) {
232241
query += ` y${year}: contributionsCollection(from: "${year}-01-01T00:00:00Z", to: "${year}-12-31T23:59:59Z") {
233242
totalCommitContributions

0 commit comments

Comments
 (0)