Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds rejectRateLimitedCalls option for WebClient #599

Merged
merged 6 commits into from Aug 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 42 additions & 4 deletions docs/_pages/web_client.md
Expand Up @@ -231,15 +231,53 @@ const web = new WebClient(token, {

### Rate limit handling

When your application has exceeded the [rate limit](https://api.slack.com/docs/rate-limits#web) for a certain method,
the `WebClient` object will emit a `rate_limited` event. Observing this event can be useful for scheduling work to be
done in the future.
Typically, you shouldn't have to worry about rate limits. By default, the `WebClient` will automatically wait the
appropriate amount of time and retry the request. During that time, all new requests from the `WebClient` will be
paused, so it doesn't make your rate-limiting problem worse. Then, once a successful response is received, the returned
Promise is resolved with the result.

In addition, you can observe when your application has been rate-limited by attaching a handler to the `rate_limited`
event.

```javascript
const { WebClient } = require('@slack/client');
const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);
web.on('rate_limited', retryAfter => console.log(`Delay future requests by at least ${retryAfter} seconds`));
web.on('rate_limited', (retryAfter) => {
console.log(`A request was rate limited and future requests will be paused for ${retryAfter} seconds`);
});

const userIds = []; // a potentially long list of user IDs
for (user of userIds) {
// if this list is large enough and responses are fast enough, this might trigger a rate-limit
// but you will get each result without any additional code, since the rate-limited requests will be retried
web.users.info({ user }).then(console.log).catch(console.error);
}
```

If you'd like to handle rate-limits in a specific way for your application, you can turn off the automatic retrying of
rate-limited API calls with the `rejectRateLimitedCalls` configuration option.

```javascript
const { WebClient, ErrorCode } = require('@slack/client');
const token = process.env.SLACK_TOKEN;
const web = new WebClient(token, { rejectRateLimitedCalls: true });

const userIds = []; // a potentially long list of user IDs
for (user of userIds) {
web.users.info({ user }).then(console.log).catch((error) => {
if (error.code === ErrorCodes.RateLimitedError) {
// the request was rate-limited, you can deal with this error in your application however you wish
console.log(
`The users.info with ID ${user} failed due to rate limiting. ` +
`The request can be retried in ${error.retryAfter} seconds.`
);
} else {
// some other error occurred
console.error(error.message);
}
});
}
```

---
Expand Down
98 changes: 76 additions & 22 deletions src/WebClient.spec.js
Expand Up @@ -615,44 +615,98 @@ describe('WebClient', function () {
});

describe('has rate limit handling', function () {
// NOTE: Check issue #451
it('should expose retry headers in the response');
it('should allow rate limit triggered retries to be turned off');
describe('when configured to reject rate-limited calls', function () {
beforeEach(function () {
this.client = new WebClient(token, { rejectRateLimitedCalls: true });
});

describe('when a request fails due to rate-limiting', function () {
// NOTE: is this retrying configurable with the retry policy? is it subject to the request concurrency?
it('should automatically retry the request after the specified timeout', function () {
it('should reject with a WebAPIRateLimitedError when a request fails due to rate-limiting', function (done) {
const retryAfter = 5;
const scope = nock('https://slack.com')
.post(/api/)
.reply(429, {}, { 'retry-after': 1 })
.post(/api/)
.reply(200, { ok: true });
const client = new WebClient(token, { retryConfig: rapidRetryPolicy });
const startTime = new Date().getTime();
return client.apiCall('method')
.then((resp) => {
const time = new Date().getTime() - startTime;
assert.isAtLeast(time, 1000, 'elapsed time is at least a second');
assert.propertyVal(resp, 'ok', true);
.reply(429, '', { 'retry-after': retryAfter });
this.client.apiCall('method')
.catch((error) => {
assert.instanceOf(error, Error);
assert.equal(error.code, ErrorCode.RateLimitedError);
assert.equal(error.retryAfter, retryAfter);
scope.done();
done();
});
});

it('should pause the remaining requests in queue');

it('should emit a rate_limited event on the client', function() {
it('should emit a rate_limited event on the client', function (done) {
const spy = sinon.spy();
const scope = nock('https://slack.com')
.post(/api/)
.reply(429, {}, { 'retry-after': 0 });
const client = new WebClient(token, { retryConfig: { retries: 0 } });
const client = new WebClient(token, { rejectRateLimitedCalls: true });
client.on('rate_limited', spy);
return client.apiCall('method')
client.apiCall('method')
.catch((err) => {
sinon.assert.calledOnce(spy);
assert(spy.calledOnceWith(0))
scope.done();
done();
});
});
});

it('should automatically retry the request after the specified timeout', function () {
const retryAfter = 1;
const scope = nock('https://slack.com')
.post(/api/)
.reply(429, '', { 'retry-after': retryAfter })
.post(/api/)
.reply(200, { ok: true });
const client = new WebClient(token, { retryConfig: rapidRetryPolicy });
const startTime = Date.now();
return client.apiCall('method')
.then(() => {
const diff = Date.now() - startTime;
assert.isAtLeast(diff, retryAfter * 1000, 'elapsed time is at least a second');
scope.done();
});
});

it('should pause the remaining requests in queue', function () {
const startTime = Date.now();
const retryAfter = 1;
const scope = nock('https://slack.com')
.post(/api/)
.reply(429, '', { 'retry-after': retryAfter })
.post(/api/)
.reply(200, function (uri, requestBody) {
return JSON.stringify({ ok: true, diff: Date.now() - startTime });
})
.post(/api/)
.reply(200, function (uri, requestBody) {
return JSON.stringify({ ok: true, diff: Date.now() - startTime });
});
const client = new WebClient(token, { retryConfig: rapidRetryPolicy, maxRequestConcurrency: 1 });
const firstCall = client.apiCall('method');
const secondCall = client.apiCall('method');
return Promise.all([firstCall, secondCall])
.then(([firstResult, secondResult]) => {
assert.isAtLeast(firstResult.diff, retryAfter * 1000);
assert.isAtLeast(secondResult.diff, retryAfter * 1000);
scope.done();
});
});

it('should emit a rate_limited event on the client', function (done) {
const spy = sinon.spy();
const scope = nock('https://slack.com')
.post(/api/)
.reply(429, {}, { 'retry-after': 0 });
const client = new WebClient(token, { retryConfig: { retries: 0 } });
client.on('rate_limited', spy);
client.apiCall('method')
.catch((err) => {
assert(spy.calledOnceWith(0))
scope.done();
done();
});
});
});

describe('has support for automatic pagination', function () {
Expand Down