Skip to content

Commit 863d3d6

Browse files
SGrondingr2m
authored andcommitted
feat(#8): onAbuseLimit/onRateLimit
BREAKING CHANGE: Changed `octokit.throttle.on('abuse-limit', handler)` and `'rate-limit'` to ``` const octokit = new Octokit({ throttle: { onAbuseLimit: handler, onRateLimit: handler } }); ```
1 parent 9efda86 commit 863d3d6

File tree

8 files changed

+287
-150
lines changed

8 files changed

+287
-150
lines changed

README.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,31 @@ Implements all [recommended best practises](https://developer.github.com/v3/guid
1111

1212
## Usage
1313

14-
The code below creates a "Hello, world!" issue on every repository in a given organization. Without the throttling plugin it would send many requests in parallel and would hit rate limits very quickly. But the `@octokit/plugin-throttling` makes sure that no requests using the same authentication token are throttled correctly.
14+
The code below creates a "Hello, world!" issue on every repository in a given organization. Without the throttling plugin it would send many requests in parallel and would hit rate limits very quickly. But the `@octokit/plugin-throttling` slows down your requests according to the official guidelines, so you don't get blocked before your quota is exhausted.
15+
16+
The `throttle.onAbuseLimit` and `throttle.onRateLimit` options are required. Return `true` to automatically retry the request after `retryAfter` seconds.
1517

1618
```js
17-
const Octokit = require('@ocotkit/rest')
19+
const Octokit = require('@octokit/rest')
1820
.plugin(require('@octokit/plugin-throttling'))
1921

20-
const octokit = new Octokit()
22+
const octokit = new Octokit({
23+
throttle: {
24+
onRateLimit: (retryAfter, options) => {
25+
console.warn(`Request quota exhausted for request ${options.method} ${options.url}`)
26+
27+
if (options.request.retryCount === 0) { // only retries once
28+
console.log(`Retrying after ${retryAfter} seconds!`)
29+
return true
30+
}
31+
},
32+
onAbuseLimit: (retryAfter, options) => {
33+
// does not retry, only logs a warning
34+
console.warn(`Abuse detected for request ${options.method} ${options.url}`)
35+
}
36+
}
37+
})
38+
2139
octokit.authenticate({
2240
type: 'token',
2341
token: process.env.TOKEN
@@ -35,13 +53,6 @@ async function createIssueOnAllRepos (org) {
3553
}
3654
```
3755

38-
Handle events
39-
40-
```js
41-
octokit.throttle.on('rate-limit', (retryAfter) => console.warn(`Rate-limit hit, retrying after ${retryAfter}s`))
42-
octokit.throttle.on('abuse-limit', (retryAfter) => console.warn(`Abuse-limit hit, retrying after ${retryAfter}s`))
43-
```
44-
4556
## LICENSE
4657

4758
[MIT](LICENSE)

lib/index.js

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ const wrapRequest = require('./wrap-request')
55

66
const triggersNotificationPaths = require('./triggers-notification-paths')
77

8-
function throttlingPlugin (octokit) {
9-
const state = {
8+
function throttlingPlugin (octokit, octokitOptions = {}) {
9+
const state = Object.assign({
1010
triggersNotification: triggersNotificationPaths,
1111
minimumAbuseRetryAfter: 5,
12-
maxRetries: 1,
12+
retryAfterBaseValue: 1000,
1313
globalLimiter: new Bottleneck({
1414
maxConcurrent: 1
1515
}),
@@ -20,13 +20,67 @@ function throttlingPlugin (octokit) {
2020
triggersNotificationLimiter: new Bottleneck({
2121
maxConcurrent: 1,
2222
minTime: 3000
23-
})
24-
}
23+
}),
24+
retryLimiter: new Bottleneck()
25+
}, octokitOptions.throttle)
26+
27+
if (typeof state.onAbuseLimit !== 'function' || typeof state.onRateLimit !== 'function') {
28+
throw new Error(`octokit/plugin-throttling error:
29+
You must pass the onAbuseLimit and onRateLimit error handlers.
30+
See https://github.com/octokit/rest.js#throttling
2531
26-
octokit.throttle = {
27-
_options: (options = {}) => Object.assign(state, options)
32+
const octokit = new Octokit({
33+
throttle: {
34+
onAbuseLimit: (error, options) => {/* ... */},
35+
onRateLimit: (error, options) => {/* ... */}
36+
}
37+
})
38+
`)
2839
}
29-
const emitter = new Bottleneck.Events(octokit.throttle)
3040

31-
octokit.hook.wrap('request', wrapRequest.bind(null, state, emitter))
41+
const events = {}
42+
const emitter = new Bottleneck.Events(events)
43+
events.on('abuse-limit', state.onAbuseLimit)
44+
events.on('rate-limit', state.onRateLimit)
45+
events.on('error', e => console.warn('Error in throttling-plugin limit handler', e))
46+
47+
state.retryLimiter.on('failed', async function (error, info) {
48+
if (error.status !== 403) {
49+
return
50+
}
51+
52+
const options = info.args[info.args.length - 1]
53+
const retryCount = ~~options.request.retryCount
54+
options.request.retryCount = retryCount
55+
56+
const { wantRetry, retryAfter } = await (async function () {
57+
if (/\babuse\b/i.test(error.message)) {
58+
// The user has hit the abuse rate limit.
59+
// https://developer.github.com/v3/#abuse-rate-limits
60+
61+
// The Retry-After header can sometimes be blank when hitting an abuse limit,
62+
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
63+
const retryAfter = Math.max(~~error.headers['retry-after'], state.minimumAbuseRetryAfter)
64+
const wantRetry = await emitter.trigger('abuse-limit', retryAfter, options)
65+
return { wantRetry, retryAfter }
66+
}
67+
if (error.headers['x-ratelimit-remaining'] === '0') {
68+
// The user has used all their allowed calls for the current time period
69+
// https://developer.github.com/v3/#rate-limiting
70+
71+
const rateLimitReset = new Date(~~error.headers['x-ratelimit-reset'] * 1000).getTime()
72+
const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0)
73+
const wantRetry = await emitter.trigger('rate-limit', retryAfter, options)
74+
return { wantRetry, retryAfter }
75+
}
76+
return {}
77+
})()
78+
79+
if (wantRetry) {
80+
options.request.retryCount++
81+
return retryAfter * state.retryAfterBaseValue
82+
}
83+
})
84+
85+
octokit.hook.wrap('request', wrapRequest.bind(null, state))
3286
}

lib/wrap-request.js

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
/* eslint padded-blocks: 0 */
21
module.exports = wrapRequest
32

43
const noop = () => Promise.resolve()
54

6-
async function wrapRequest (state, emitter, request, options) {
7-
const retryRequest = function (after) {
8-
return new Promise(resolve => setTimeout(resolve, after * 1000))
9-
.then(() => wrapRequest(state, emitter, request, options))
10-
}
5+
function wrapRequest (state, request, options) {
6+
return state.retryLimiter.schedule(doRequest, state, request, options)
7+
}
8+
9+
async function doRequest (state, request, options) {
1110
const isWrite = options.method !== 'GET' && options.method !== 'HEAD'
1211
const retryCount = ~~options.request.retryCount
1312
const jobOptions = retryCount > 0 ? { priority: 0, weight: 0 } : {}
@@ -22,40 +21,5 @@ async function wrapRequest (state, emitter, request, options) {
2221
await state.triggersNotificationLimiter.schedule(jobOptions, noop)
2322
}
2423

25-
return state.globalLimiter.schedule(jobOptions, async function () {
26-
try {
27-
// Execute request
28-
return await request(options)
29-
} catch (error) {
30-
if (error.status === 403 && /\babuse\b/i.test(error.message)) {
31-
// The user has hit the abuse rate limit.
32-
// https://developer.github.com/v3/#abuse-rate-limits
33-
34-
// The Retry-After header can sometimes be blank when hitting an abuse limit,
35-
// but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
36-
const retryAfter = Math.max(~~error.headers['retry-after'], state.minimumAbuseRetryAfter)
37-
emitter.trigger('abuse-limit', retryAfter)
38-
39-
if (state.maxRetries > retryCount) {
40-
options.request.retryCount = retryCount + 1
41-
return retryRequest(retryAfter)
42-
}
43-
44-
} else if (error.status === 403 && error.headers['x-ratelimit-remaining'] === '0') {
45-
// The user has used all their allowed calls for the current time period
46-
// https://developer.github.com/v3/#rate-limiting
47-
48-
const rateLimitReset = new Date(~~error.headers['x-ratelimit-reset'] * 1000).getTime()
49-
const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0)
50-
emitter.trigger('rate-limit', retryAfter)
51-
52-
if (state.maxRetries > retryCount) {
53-
options.request.retryCount = retryCount + 1
54-
return retryRequest(retryAfter)
55-
}
56-
}
57-
58-
throw error
59-
}
60-
})
24+
return state.globalLimiter.schedule(jobOptions, request, options)
6125
}

package-lock.json

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
]
3636
},
3737
"dependencies": {
38-
"bottleneck": "^2.14.1"
38+
"bottleneck": "^2.15.0"
3939
},
4040
"devDependencies": {
4141
"@octokit/request": "2.2.0",

0 commit comments

Comments
 (0)