Skip to content

Commit 323a08d

Browse files
authored
fix: do not retry GraphQl request in case for 401 (#295)
This updates the bottleneck "failed" handler to not retry GraphQL requests that receive a 401 response. The old behavior was to retry all GraphQL error responses, which caused the retry to be throttled for an hour, due to 401 Unauthorized responses including an `x-ratelimit-remaining` of `0` and a `x-ratelimit-reset` of now + 3600 seconds. The main issue was that the request wasn't being ratelimited, but the ratelimit headers caused the plugin to wait. In testing this out, I noticed that GraphQL responses can also receive a 403 Forbidden response when hitting the abuse rate limit. This commit keeps the functionality for retrying 403 responses for GraphQL requests, since they signify abuse rate limiting, where 401 responses indicate bad credentials.
1 parent 9e8f288 commit 323a08d

File tree

3 files changed

+97
-6
lines changed

3 files changed

+97
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![@latest](https://img.shields.io/npm/v/@octokit/plugin-throttling.svg)](https://www.npmjs.com/package/@octokit/plugin-throttling)
66
[![Build Status](https://github.com/octokit/plugin-throttling.js/workflows/Test/badge.svg)](https://github.com/octokit/plugin-throttling.js/actions?workflow=Test)
77

8-
Implements all [recommended best practises](https://developer.github.com/v3/guides/best-practices-for-integrators/) to prevent hitting abuse rate limits.
8+
Implements all [recommended best practises](https://docs.github.com/en/rest/guides/best-practices-for-integrators) to prevent hitting abuse rate limits.
99

1010
## Usage
1111

src/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ export function throttling(octokit: Octokit, octokitOptions = {}) {
109109
// @ts-ignore
110110
state.retryLimiter.on("failed", async function (error, info) {
111111
const options = info.args[info.args.length - 1];
112-
const isGraphQL = options.url.startsWith("/graphql");
112+
const shouldRetryGraphQL =
113+
options.url.startsWith("/graphql") && error.status !== 401;
113114

114-
if (!(isGraphQL || error.status === 403)) {
115+
if (!(shouldRetryGraphQL || error.status === 403)) {
115116
return;
116117
}
117118

@@ -120,8 +121,8 @@ export function throttling(octokit: Octokit, octokitOptions = {}) {
120121

121122
const { wantRetry, retryAfter } = await (async function () {
122123
if (/\babuse\b/i.test(error.message)) {
123-
// The user has hit the abuse rate limit. (REST only)
124-
// https://developer.github.com/v3/#abuse-rate-limits
124+
// The user has hit the abuse rate limit. (REST and GraphQL)
125+
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#abuse-rate-limits
125126

126127
// The Retry-After header can sometimes be blank when hitting an abuse limit,
127128
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
@@ -142,7 +143,8 @@ export function throttling(octokit: Octokit, octokitOptions = {}) {
142143
error.headers["x-ratelimit-remaining"] === "0"
143144
) {
144145
// The user has used all their allowed calls for the current time period (REST and GraphQL)
145-
// https://developer.github.com/v3/#rate-limiting
146+
// https://docs.github.com/en/rest/reference/rate-limit (REST)
147+
// https://docs.github.com/en/graphql/overview/resource-limitations#rate-limit (GraphQL)
146148

147149
const rateLimitReset = new Date(
148150
~~error.headers["x-ratelimit-reset"] * 1000

test/retry.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,5 +251,94 @@ describe("Retry", function () {
251251
"END POST /graphql",
252252
]);
253253
});
254+
255+
it("Should ignore 401 Unauthorized errors", async function () {
256+
let eventCount = 0;
257+
const octokit = new TestOctokit({
258+
throttle: {
259+
write: new Bottleneck.Group({ minTime: 50 }),
260+
onRateLimit: (retryAfter: number, options: object) => {
261+
eventCount++;
262+
return true;
263+
},
264+
onAbuseLimit: () => 1,
265+
},
266+
});
267+
268+
try {
269+
await octokit.request("POST /graphql", {
270+
request: {
271+
responses: [
272+
{
273+
status: 401,
274+
headers: {
275+
"x-ratelimit-remaining": "0",
276+
"x-ratelimit-reset": "123",
277+
},
278+
data: {
279+
message: "Bad credentials",
280+
documentation_url: "https://docs.github.com/graphql",
281+
},
282+
},
283+
],
284+
},
285+
});
286+
throw new Error("Should not reach this point");
287+
} catch (error) {
288+
expect(error.status).toEqual(401);
289+
expect(error.message).toEqual("Bad credentials");
290+
}
291+
292+
expect(eventCount).toEqual(0);
293+
expect(octokit.__requestLog).toStrictEqual(["START POST /graphql"]);
294+
});
295+
296+
it("Should retry 403 Forbidden errors on abuse limit", async function () {
297+
let eventCount = 0;
298+
const octokit = new TestOctokit({
299+
throttle: {
300+
write: new Bottleneck.Group({ minTime: 50 }),
301+
onAbuseLimit: (retryAfter: number, options: object) => {
302+
eventCount++;
303+
return true;
304+
},
305+
onRateLimit: () => 1,
306+
minimumAbuseRetryAfter: 0,
307+
retryAfterBaseValue: 50,
308+
},
309+
});
310+
const res = await octokit.request("POST /graphql", {
311+
request: {
312+
responses: [
313+
{
314+
status: 403,
315+
headers: {
316+
"retry-after": 1,
317+
},
318+
data: {
319+
message:
320+
"You have triggered an abuse detection mechanism. Please wait a few minutes before you try again.",
321+
documentation_url:
322+
"https://developer.github.com/v3/#abuse-rate-limits",
323+
},
324+
},
325+
{ status: 200, headers: {}, data: { message: "Success!" } },
326+
],
327+
},
328+
});
329+
330+
expect(res.status).toEqual(200);
331+
expect(res.data).toMatchObject({ message: "Success!" });
332+
expect(eventCount).toEqual(1);
333+
expect(octokit.__requestLog).toStrictEqual([
334+
"START POST /graphql",
335+
"START POST /graphql",
336+
"END POST /graphql",
337+
]);
338+
339+
const ms = octokit.__requestTimings[1] - octokit.__requestTimings[0];
340+
expect(ms).toBeLessThan(80);
341+
expect(ms).toBeGreaterThan(20);
342+
});
254343
});
255344
});

0 commit comments

Comments
 (0)