Skip to content

Commit 5335870

Browse files
authoredMay 19, 2023
fix: bump minimal retry in case of secondary rate limiting to 60s (#594)
1 parent 28098cb commit 5335870

File tree

5 files changed

+102
-169
lines changed

5 files changed

+102
-169
lines changed
 

‎src/index.ts

+29-23
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,34 @@ export function throttling(octokit: Octokit, octokitOptions: OctokitOptions) {
5959
createGroups(Bottleneck, common);
6060
}
6161

62+
if (
63+
octokitOptions.throttle &&
64+
octokitOptions.throttle.minimalSecondaryRateRetryAfter
65+
) {
66+
octokit.log.warn(
67+
"[@octokit/plugin-throttling] `options.throttle.minimalSecondaryRateRetryAfter` is deprecated, please use `options.throttle.fallbackSecondaryRateRetryAfter` instead"
68+
);
69+
octokitOptions.throttle.fallbackSecondaryRateRetryAfter =
70+
octokitOptions.throttle.minimalSecondaryRateRetryAfter;
71+
delete octokitOptions.throttle.minimalSecondaryRateRetryAfter;
72+
}
73+
74+
if (octokitOptions.throttle && octokitOptions.throttle.onAbuseLimit) {
75+
octokit.log.warn(
76+
"[@octokit/plugin-throttling] `onAbuseLimit()` is deprecated and will be removed in a future release of `@octokit/plugin-throttling`, please use the `onSecondaryRateLimit` handler instead"
77+
);
78+
// @ts-ignore types don't allow for both properties to be set
79+
octokitOptions.throttle.onSecondaryRateLimit =
80+
octokitOptions.throttle.onAbuseLimit;
81+
// @ts-ignore
82+
delete octokitOptions.throttle.onAbuseLimit;
83+
}
84+
6285
const state = Object.assign(
6386
{
6487
clustering: connection != null,
6588
triggersNotification,
66-
minimumSecondaryRateRetryAfter: 5,
89+
fallbackSecondaryRateRetryAfter: 60,
6790
retryAfterBaseValue: 1000,
6891
retryLimiter: new Bottleneck(),
6992
id,
@@ -72,13 +95,8 @@ export function throttling(octokit: Octokit, octokitOptions: OctokitOptions) {
7295
octokitOptions.throttle
7396
);
7497

75-
const isUsingDeprecatedOnAbuseLimitHandler =
76-
typeof state.onAbuseLimit === "function" && state.onAbuseLimit;
77-
7898
if (
79-
typeof (isUsingDeprecatedOnAbuseLimitHandler
80-
? state.onAbuseLimit
81-
: state.onSecondaryRateLimit) !== "function" ||
99+
typeof state.onSecondaryRateLimit !== "function" ||
82100
typeof state.onRateLimit !== "function"
83101
) {
84102
throw new Error(`octokit/plugin-throttling error:
@@ -97,18 +115,7 @@ export function throttling(octokit: Octokit, octokitOptions: OctokitOptions) {
97115
const events = {};
98116
const emitter = new Bottleneck.Events(events);
99117
// @ts-expect-error
100-
events.on(
101-
"secondary-limit",
102-
isUsingDeprecatedOnAbuseLimitHandler
103-
? function (...args: [number, OctokitOptions, Octokit]) {
104-
octokit.log.warn(
105-
"[@octokit/plugin-throttling] `onAbuseLimit()` is deprecated and will be removed in a future release of `@octokit/plugin-throttling`, please use the `onSecondaryRateLimit` handler instead"
106-
);
107-
// @ts-expect-error
108-
return state.onAbuseLimit(...args);
109-
}
110-
: state.onSecondaryRateLimit
111-
);
118+
events.on("secondary-limit", state.onSecondaryRateLimit);
112119
// @ts-expect-error
113120
events.on("rate-limit", state.onRateLimit);
114121
// @ts-expect-error
@@ -140,10 +147,9 @@ export function throttling(octokit: Octokit, octokitOptions: OctokitOptions) {
140147

141148
// The Retry-After header can sometimes be blank when hitting a secondary rate limit,
142149
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
143-
const retryAfter = Math.max(
144-
~~error.response.headers["retry-after"],
145-
state.minimumSecondaryRateRetryAfter
146-
);
150+
const retryAfter =
151+
Number(error.response.headers["retry-after"]) ||
152+
state.fallbackSecondaryRateRetryAfter;
147153
const wantRetry = await emitter.trigger(
148154
"secondary-limit",
149155
retryAfter,

‎src/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export type ThrottlingOptionsBase = {
3030
id?: string;
3131
timeout?: number;
3232
connection?: Bottleneck.RedisConnection | Bottleneck.IORedisConnection;
33-
minimumSecondaryRateRetryAfter?: number;
33+
/**
34+
* @deprecated use `fallbackSecondaryRateRetryAfter`
35+
*/
36+
minimalSecondaryRateRetryAfter?: number;
37+
fallbackSecondaryRateRetryAfter?: number;
3438
retryAfterBaseValue?: number;
3539
write?: Bottleneck.Group;
3640
search?: Bottleneck.Group;

‎test/deprecations.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Octokit } from "@octokit/core";
2+
import { throttling } from "../src";
3+
4+
const TestOctokit = Octokit.plugin(throttling);
5+
6+
describe("deprecations", () => {
7+
it("throttle.minimalSecondaryRateRetryAfter option", () => {
8+
const log = {
9+
warn: jest.fn(),
10+
};
11+
new TestOctokit({
12+
// @ts-expect-error
13+
log,
14+
throttle: {
15+
minimalSecondaryRateRetryAfter: 1,
16+
onSecondaryRateLimit: () => 1,
17+
onRateLimit: () => 1,
18+
},
19+
});
20+
21+
expect(log.warn).toHaveBeenCalledWith(
22+
"[@octokit/plugin-throttling] `options.throttle.minimalSecondaryRateRetryAfter` is deprecated, please use `options.throttle.fallbackSecondaryRateRetryAfter` instead"
23+
);
24+
});
25+
26+
describe("throttle.onAbuseLimit", function () {
27+
it("Should detect SecondaryRate limit and broadcast event", async function () {
28+
const log = {
29+
warn: jest.fn(),
30+
};
31+
32+
new TestOctokit({
33+
// @ts-expect-error
34+
log,
35+
throttle: {
36+
onAbuseLimit: () => 1,
37+
onRateLimit: () => 1,
38+
},
39+
});
40+
41+
expect(log.warn).toHaveBeenCalledWith(
42+
"[@octokit/plugin-throttling] `onAbuseLimit()` is deprecated and will be removed in a future release of `@octokit/plugin-throttling`, please use the `onSecondaryRateLimit` handler instead"
43+
);
44+
});
45+
});
46+
});

‎test/events.test.ts

+8-140
Original file line numberDiff line numberDiff line change
@@ -80,56 +80,12 @@ describe("Events", function () {
8080
expect(eventCount).toEqual(1);
8181
});
8282

83-
it("Should ensure retryAfter is a minimum of 5s", async function () {
83+
it("Should broadcast retryAfter of 60s even when the header is missing", async function () {
8484
let eventCount = 0;
8585
const octokit = new TestOctokit({
8686
throttle: {
8787
onSecondaryRateLimit: (retryAfter, options) => {
88-
expect(retryAfter).toEqual(5);
89-
expect(options).toMatchObject({
90-
method: "GET",
91-
url: "/route2",
92-
request: { retryCount: 0 },
93-
});
94-
eventCount++;
95-
},
96-
onRateLimit: () => 1,
97-
},
98-
});
99-
100-
await octokit.request("GET /route1", {
101-
request: {
102-
responses: [{ status: 201, headers: {}, data: {} }],
103-
},
104-
});
105-
try {
106-
await octokit.request("GET /route2", {
107-
request: {
108-
responses: [
109-
{
110-
status: 403,
111-
headers: { "retry-after": "2" },
112-
data: {
113-
message: "You have exceeded a secondary rate limit",
114-
},
115-
},
116-
],
117-
},
118-
});
119-
throw new Error("Should not reach this point");
120-
} catch (error: any) {
121-
expect(error.status).toEqual(403);
122-
}
123-
124-
expect(eventCount).toEqual(1);
125-
});
126-
127-
it("Should broadcast retryAfter of 5s even when the header is missing", async function () {
128-
let eventCount = 0;
129-
const octokit = new TestOctokit({
130-
throttle: {
131-
onSecondaryRateLimit: (retryAfter, options) => {
132-
expect(retryAfter).toEqual(5);
88+
expect(retryAfter).toEqual(60);
13389
expect(options).toMatchObject({
13490
method: "GET",
13591
url: "/route2",
@@ -168,13 +124,13 @@ describe("Events", function () {
168124
expect(eventCount).toEqual(1);
169125
});
170126
});
171-
describe("with 'onAbuseLimit'", function () {
127+
describe("with 'onSecondaryRateLimit'", function () {
172128
it("Should detect SecondaryRate limit and broadcast event", async function () {
173129
let eventCount = 0;
174130

175131
const octokit = new TestOctokit({
176132
throttle: {
177-
onAbuseLimit: (retryAfter, options, octokitFromOptions) => {
133+
onSecondaryRateLimit: (retryAfter, options, octokitFromOptions) => {
178134
expect(octokit).toBe(octokitFromOptions);
179135
expect(retryAfter).toEqual(60);
180136
expect(options).toMatchObject({
@@ -214,94 +170,6 @@ describe("Events", function () {
214170

215171
expect(eventCount).toEqual(1);
216172
});
217-
218-
it("Should ensure retryAfter is a minimum of 5s", async function () {
219-
let eventCount = 0;
220-
const octokit = new TestOctokit({
221-
throttle: {
222-
onAbuseLimit: (retryAfter, options) => {
223-
expect(retryAfter).toEqual(5);
224-
expect(options).toMatchObject({
225-
method: "GET",
226-
url: "/route2",
227-
request: { retryCount: 0 },
228-
});
229-
eventCount++;
230-
},
231-
onRateLimit: () => 1,
232-
},
233-
});
234-
235-
await octokit.request("GET /route1", {
236-
request: {
237-
responses: [{ status: 201, headers: {}, data: {} }],
238-
},
239-
});
240-
try {
241-
await octokit.request("GET /route2", {
242-
request: {
243-
responses: [
244-
{
245-
status: 403,
246-
headers: { "retry-after": "2" },
247-
data: {
248-
message: "You have exceeded a secondary rate limit",
249-
},
250-
},
251-
],
252-
},
253-
});
254-
throw new Error("Should not reach this point");
255-
} catch (error: any) {
256-
expect(error.status).toEqual(403);
257-
}
258-
259-
expect(eventCount).toEqual(1);
260-
});
261-
262-
it("Should broadcast retryAfter of 5s even when the header is missing", async function () {
263-
let eventCount = 0;
264-
const octokit = new TestOctokit({
265-
throttle: {
266-
onAbuseLimit: (retryAfter, options) => {
267-
expect(retryAfter).toEqual(5);
268-
expect(options).toMatchObject({
269-
method: "GET",
270-
url: "/route2",
271-
request: { retryCount: 0 },
272-
});
273-
eventCount++;
274-
},
275-
onRateLimit: () => 1,
276-
},
277-
});
278-
279-
await octokit.request("GET /route1", {
280-
request: {
281-
responses: [{ status: 201, headers: {}, data: {} }],
282-
},
283-
});
284-
try {
285-
await octokit.request("GET /route2", {
286-
request: {
287-
responses: [
288-
{
289-
status: 403,
290-
headers: {},
291-
data: {
292-
message: "You have exceeded a secondary rate limit",
293-
},
294-
},
295-
],
296-
},
297-
});
298-
throw new Error("Should not reach this point");
299-
} catch (error: any) {
300-
expect(error.status).toEqual(403);
301-
}
302-
303-
expect(eventCount).toEqual(1);
304-
});
305173
});
306174
});
307175

@@ -360,15 +228,15 @@ describe("Events", function () {
360228
const octokit = new TestOctokit({
361229
throttle: {
362230
onRateLimit: () => {
363-
throw new Error("Should not reach this point");
231+
throw new Error("Error in onRateLimit handler");
364232
},
365233
onSecondaryRateLimit: () => {
366-
throw new Error("Should not reach this point");
234+
throw new Error("Error in onSecondaryRateLimit handler");
367235
},
368236
},
369237
});
370238

371-
jest.spyOn(octokit.log, "warn");
239+
jest.spyOn(octokit.log, "warn").mockImplementation(() => {});
372240

373241
const t0 = Date.now();
374242

@@ -397,7 +265,7 @@ describe("Events", function () {
397265
expect(error.status).toEqual(403);
398266
expect(octokit.log.warn).toHaveBeenCalledWith(
399267
"Error in throttling-plugin limit handler",
400-
new Error("Should not reach this point")
268+
new Error("Error in onRateLimit handler")
401269
);
402270
}
403271
});

‎test/retry.test.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import { throttling } from "../src";
55
import { AddressInfo } from "net";
66
import { createServer } from "http";
77

8+
jest.setTimeout(20000);
9+
810
describe("Retry", function () {
911
describe("REST", function () {
1012
it("Should retry 'secondary-limit' and succeed", async function () {
1113
let eventCount = 0;
1214
const octokit = new TestOctokit({
1315
throttle: {
14-
minimumSecondaryRateRetryAfter: 0,
16+
fallbackSecondaryRateRetryAfter: 0,
1517
retryAfterBaseValue: 50,
1618
onSecondaryRateLimit: (retryAfter, options) => {
1719
expect(options).toMatchObject({
@@ -57,7 +59,7 @@ describe("Retry", function () {
5759
let eventCount = 0;
5860
const octokit = new TestOctokit({
5961
throttle: {
60-
minimumSecondaryRateRetryAfter: 0,
62+
fallbackSecondaryRateRetryAfter: 0,
6163
retryAfterBaseValue: 50,
6264
onSecondaryRateLimit: (retryAfter, options) => {
6365
expect(options).toMatchObject({
@@ -147,7 +149,7 @@ describe("Retry", function () {
147149
const octokit = new ThrottledOctokit({
148150
baseUrl: `http://localhost:${port}`,
149151
throttle: {
150-
minimumSecondaryRateRetryAfter: 0,
152+
fallbackSecondaryRateRetryAfter: 0,
151153
retryAfterBaseValue: 50,
152154
onRateLimit: () => true,
153155
onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
@@ -163,7 +165,14 @@ describe("Retry", function () {
163165
await octokit.request("GET /nope-nope-ok");
164166
await octokit.request("GET /nope-nope-ok");
165167
} finally {
166-
server.close();
168+
return new Promise((resolve, reject) => {
169+
server.close((error) => {
170+
if (error) {
171+
return reject(error);
172+
}
173+
resolve("ok");
174+
});
175+
});
167176
}
168177
});
169178

@@ -405,7 +414,7 @@ describe("Retry", function () {
405414
return true;
406415
},
407416
onRateLimit: () => 1,
408-
minimumSecondaryRateRetryAfter: 0,
417+
fallbackSecondaryRateRetryAfter: 0,
409418
retryAfterBaseValue: 50,
410419
},
411420
});

0 commit comments

Comments
 (0)
Please sign in to comment.