From 4f3fa6acb035f5816259a2d3f8bc121fbea73d7c Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 3 Aug 2018 18:48:14 -0700 Subject: [PATCH 1/5] add WebAPIRateLimitedError and rejectRateLimitedCalls option on WebClient --- src/WebClient.ts | 35 ++++++++++++++++++++++++++++++++--- src/errors.ts | 1 + 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/WebClient.ts b/src/WebClient.ts index 7a6f60550..c0bbca87f 100644 --- a/src/WebClient.ts +++ b/src/WebClient.ts @@ -64,6 +64,11 @@ export class WebClient extends EventEmitter { */ private pageSize: number; + /** + * Preference for immediately rejecting API calls which result in a rate-limited response + */ + private rejectRateLimitedCalls: boolean; + /** * The name used to prefix all logging generated from this object */ @@ -91,6 +96,7 @@ export class WebClient extends EventEmitter { agent = undefined, tls = undefined, pageSize = 200, + rejectRateLimitedCalls = false, }: WebClientOptions = {}) { super(); this.token = token; @@ -102,6 +108,7 @@ export class WebClient extends EventEmitter { // NOTE: may want to filter the keys to only those acceptable for TLS options this.tlsConfig = tls !== undefined ? tls : {}; this.pageSize = pageSize; + this.rejectRateLimitedCalls = rejectRateLimitedCalls; // Logging if (logger !== undefined) { @@ -206,12 +213,14 @@ export class WebClient extends EventEmitter { ), ) as got.Response; - // TODO: config option for automatic rate limit handling here if (response.statusCode === 429) { const retrySec = parseRetryHeaders(response); if (retrySec !== undefined) { this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); this.emit('rate_limited', retrySec); + if (this.rejectRateLimitedCalls) { + throw new pRetry.AbortError(rateLimitedErrorWithDelay(retrySec)); + } // pause the request queue and then delay the rejection by the amount of time in the retry header this.requestQueue.pause(); // NOTE: if there was a way to introspect the current RetryOperation and know what the next timeout @@ -696,6 +705,7 @@ export interface WebClientOptions { agent?: AgentOption; tls?: TLSOptions; pageSize?: number; + rejectRateLimitedCalls?: boolean; } // NOTE: could potentially add GotOptions to this interface (using &, or maybe as an embedded key) @@ -718,7 +728,8 @@ export interface WebAPIResultCallback { (error: WebAPICallError, result: WebAPICallResult): void; } -export type WebAPICallError = WebAPIPlatformError | WebAPIRequestError | WebAPIReadError | WebAPIHTTPError; +export type WebAPICallError = + WebAPIPlatformError | WebAPIRequestError | WebAPIReadError | WebAPIHTTPError | WebAPIRateLimitedError; export interface WebAPIPlatformError extends CodedError { code: ErrorCode.PlatformError; @@ -746,6 +757,11 @@ export interface WebAPIHTTPError extends CodedError { body?: any; } +export interface WebAPIRateLimitedError extends CodedError { + code: ErrorCode.RateLimitedError; + retryAfter: number; +} + /* * Helpers */ @@ -806,7 +822,7 @@ function httpErrorFromResponse(response: got.Response): WebAPIHTTPError new Error(`An HTTP protocol error occurred: statusCode = ${response.statusCode}`), ErrorCode.HTTPError, ) as Partial; - error.original = new Error('The WebAPIHTTPError.original property is deprecated'); + error.original = new Error('The WebAPIHTTPError.original property is deprecated. See other properties for details.'); error.statusCode = response.statusCode; error.statusMessage = response.statusMessage; error.headers = response.headers; @@ -818,6 +834,19 @@ function httpErrorFromResponse(response: got.Response): WebAPIHTTPError return (error as WebAPIHTTPError); } +/** + * A factory to create WebAPIRateLimitedError objects + * @param retrySec - Number of seconds that the request can be retried in + */ +function rateLimitedErrorWithDelay(retrySec: number): WebAPIRateLimitedError { + const error = errorWithCode( + new Error(`A rate-limit has been reached, you may retry this request in ${retrySec} seconds`), + ErrorCode.RateLimitedError, + ) as Partial; + error.retryAfter = retrySec; + return (error as WebAPIRateLimitedError); +} + enum PaginationType { Cursor = 'Cursor', Timeline = 'Timeline', diff --git a/src/errors.ts b/src/errors.ts index 19ddbfc81..d8970ef61 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -19,6 +19,7 @@ export enum ErrorCode { ReadError = 'slackclient_read_error', // Corresponds to WebAPIReadError HTTPError = 'slackclient_http_error', // Corresponds to WebAPIHTTPError PlatformError = 'slackclient_platform_error', // Corresponds to WebAPIPlatformError + RateLimitedError = 'slackclient_rate_limited_error', // Corresponds to WebAPIRateLimitedError // RTMClient RTMSendWhileDisconnectedError = 'slackclient_rtmclient_send_while_disconnected_error', From 5358c02d74ea965ca7b911dfec6660d78f6a8f3b Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 3 Aug 2018 20:20:01 -0700 Subject: [PATCH 2/5] add test for rejectRateLimitedCalls WebClient config option --- src/WebClient.spec.js | 25 +++++++++++++++++++++---- src/WebClient.ts | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/WebClient.spec.js b/src/WebClient.spec.js index 4bafbbfd1..41bfd9bb6 100644 --- a/src/WebClient.spec.js +++ b/src/WebClient.spec.js @@ -615,12 +615,29 @@ 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, retryConfig: rapidRetryPolicy }); + }); + + 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': 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(); + }); + }); + }); 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 () { const scope = nock('https://slack.com') .post(/api/) diff --git a/src/WebClient.ts b/src/WebClient.ts index c0bbca87f..3f5a7172a 100644 --- a/src/WebClient.ts +++ b/src/WebClient.ts @@ -216,11 +216,11 @@ export class WebClient extends EventEmitter { if (response.statusCode === 429) { const retrySec = parseRetryHeaders(response); if (retrySec !== undefined) { - this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); this.emit('rate_limited', retrySec); if (this.rejectRateLimitedCalls) { throw new pRetry.AbortError(rateLimitedErrorWithDelay(retrySec)); } + this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); // pause the request queue and then delay the rejection by the amount of time in the retry header this.requestQueue.pause(); // NOTE: if there was a way to introspect the current RetryOperation and know what the next timeout From 792c44de4a0067b5ed095bb60af11519b963566e Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 6 Aug 2018 15:40:00 -0700 Subject: [PATCH 3/5] refactor some rate-limiting tests, add logging to webclient --- src/WebClient.spec.js | 94 ++++++++++++++++++++++++++++++------------- src/WebClient.ts | 31 ++++++++------ 2 files changed, 85 insertions(+), 40 deletions(-) diff --git a/src/WebClient.spec.js b/src/WebClient.spec.js index 41bfd9bb6..b216853e3 100644 --- a/src/WebClient.spec.js +++ b/src/WebClient.spec.js @@ -615,10 +615,9 @@ describe('WebClient', function () { }); describe('has rate limit handling', function () { - describe('when configured to reject rate-limited calls', function () { beforeEach(function () { - this.client = new WebClient(token, { rejectRateLimitedCalls: true, retryConfig: rapidRetryPolicy }); + this.client = new WebClient(token, { rejectRateLimitedCalls: true }); }); it('should reject with a WebAPIRateLimitedError when a request fails due to rate-limiting', function (done) { @@ -635,41 +634,82 @@ describe('WebClient', function () { done(); }); }); - }); - - describe('when a request fails due to rate-limiting', function () { - it('should automatically retry the request after the specified timeout', function () { - 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); - scope.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(); + }); + }); + + // TODO: seems like the queuing logic needs to be adjusted + it.skip('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) { + console.log('first success'); + return JSON.stringify({ ok: true, diff: Date.now() - startTime }); + }) + .post(/api/) + .reply(200, function (uri, requestBody) { + console.log('second success'); + return JSON.stringify({ ok: true, diff: Date.now() - startTime }); + }); + const client = new WebClient(token, { retryConfig: rapidRetryPolicy, maxRequestConcurrency: 1, logLevel: LogLevel.DEBUG }); + 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 () { diff --git a/src/WebClient.ts b/src/WebClient.ts index 3f5a7172a..bd3c7cd15 100644 --- a/src/WebClient.ts +++ b/src/WebClient.ts @@ -198,20 +198,24 @@ export class WebClient extends EventEmitter { const task = async () => { try { const response = await this.requestQueue.add( - () => got.post(urlJoin(this.slackApiUrl, method), - // @ts-ignore - Object.assign({ - form: !canBodyBeFormMultipart(requestBody), - body: requestBody, - retries: 0, - headers: { - 'user-agent': this.userAgent, - }, - throwHttpErrors: false, - agent: this.agentConfig, - }, this.tlsConfig), - ), + () => { + this.logger.debug('will perform http request'); + return got.post(urlJoin(this.slackApiUrl, method), + // @ts-ignore + Object.assign({ + form: !canBodyBeFormMultipart(requestBody), + body: requestBody, + retries: 0, + headers: { + 'user-agent': this.userAgent, + }, + throwHttpErrors: false, + agent: this.agentConfig, + }, this.tlsConfig), + ); + }, ) as got.Response; + this.logger.debug('http response received'); if (response.statusCode === 429) { const retrySec = parseRetryHeaders(response); @@ -262,6 +266,7 @@ export class WebClient extends EventEmitter { return result; } catch (error) { + this.logger.debug('http request failed'); if (error.name === 'RequestError') { throw requestErrorWithOriginal(error); } else if (error.name === 'ReadError') { From 5197ee4a7ec26a05d42e92fbb12358516c8ce5b7 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 6 Aug 2018 16:22:36 -0700 Subject: [PATCH 4/5] move task response handling into request-queue awaited function --- src/WebClient.spec.js | 7 +-- src/WebClient.ts | 142 +++++++++++++++++++++--------------------- 2 files changed, 72 insertions(+), 77 deletions(-) diff --git a/src/WebClient.spec.js b/src/WebClient.spec.js index b216853e3..21d2dd020 100644 --- a/src/WebClient.spec.js +++ b/src/WebClient.spec.js @@ -668,8 +668,7 @@ describe('WebClient', function () { }); }); - // TODO: seems like the queuing logic needs to be adjusted - it.skip('should pause the remaining requests in queue', function () { + it('should pause the remaining requests in queue', function () { const startTime = Date.now(); const retryAfter = 1; const scope = nock('https://slack.com') @@ -677,15 +676,13 @@ describe('WebClient', function () { .reply(429, '', { 'retry-after': retryAfter }) .post(/api/) .reply(200, function (uri, requestBody) { - console.log('first success'); return JSON.stringify({ ok: true, diff: Date.now() - startTime }); }) .post(/api/) .reply(200, function (uri, requestBody) { - console.log('second success'); return JSON.stringify({ ok: true, diff: Date.now() - startTime }); }); - const client = new WebClient(token, { retryConfig: rapidRetryPolicy, maxRequestConcurrency: 1, logLevel: LogLevel.DEBUG }); + const client = new WebClient(token, { retryConfig: rapidRetryPolicy, maxRequestConcurrency: 1 }); const firstCall = client.apiCall('method'); const secondCall = client.apiCall('method'); return Promise.all([firstCall, secondCall]) diff --git a/src/WebClient.ts b/src/WebClient.ts index bd3c7cd15..6e3a8b483 100644 --- a/src/WebClient.ts +++ b/src/WebClient.ts @@ -195,86 +195,84 @@ export class WebClient extends EventEmitter { options, )); - const task = async () => { - try { - const response = await this.requestQueue.add( - () => { - this.logger.debug('will perform http request'); - return got.post(urlJoin(this.slackApiUrl, method), - // @ts-ignore - Object.assign({ - form: !canBodyBeFormMultipart(requestBody), - body: requestBody, - retries: 0, - headers: { - 'user-agent': this.userAgent, - }, - throwHttpErrors: false, - agent: this.agentConfig, - }, this.tlsConfig), - ); - }, - ) as got.Response; - this.logger.debug('http response received'); - - if (response.statusCode === 429) { - const retrySec = parseRetryHeaders(response); - if (retrySec !== undefined) { - this.emit('rate_limited', retrySec); - if (this.rejectRateLimitedCalls) { - throw new pRetry.AbortError(rateLimitedErrorWithDelay(retrySec)); + const task = () => this.requestQueue.add( + async () => { + this.logger.debug('will perform http request'); + try { + const response = await got.post(urlJoin(this.slackApiUrl, method), + // @ts-ignore + Object.assign({ + form: !canBodyBeFormMultipart(requestBody), + body: requestBody, + retries: 0, + headers: { + 'user-agent': this.userAgent, + }, + throwHttpErrors: false, + agent: this.agentConfig, + }, this.tlsConfig), + ); + this.logger.debug('http response received'); + + if (response.statusCode === 429) { + const retrySec = parseRetryHeaders(response); + if (retrySec !== undefined) { + this.emit('rate_limited', retrySec); + if (this.rejectRateLimitedCalls) { + throw new pRetry.AbortError(rateLimitedErrorWithDelay(retrySec)); + } + this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); + // pause the request queue and then delay the rejection by the amount of time in the retry header + this.requestQueue.pause(); + // NOTE: if there was a way to introspect the current RetryOperation and know what the next timeout + // would be, then we could subtract that time from the following delay, knowing that it the next + // attempt still wouldn't occur until after the rate-limit header has specified. an even better + // solution would be to subtract the time from only the timeout of this next attempt of the + // RetryOperation. this would result in the staying paused for the entire duration specified in the + // header, yet this operation not having to pay the timeout cost in addition to that. + await delay(retrySec * 1000); + // resume the request queue and throw a non-abort error to signal a retry + this.requestQueue.start(); + // TODO: turn this into a WebAPIPlatformError + throw Error('A rate limit was exceeded.'); + } else { + throw new pRetry.AbortError(new Error('Retry header did not contain a valid timeout.')); } - this.logger.info(`API Call failed due to rate limiting. Will retry in ${retrySec} seconds.`); - // pause the request queue and then delay the rejection by the amount of time in the retry header - this.requestQueue.pause(); - // NOTE: if there was a way to introspect the current RetryOperation and know what the next timeout - // would be, then we could subtract that time from the following delay, knowing that it the next - // attempt still wouldn't occur until after the rate-limit header has specified. an even better - // solution would be to subtract the time from only the timeout of this next attempt of the - // RetryOperation. this would result in the staying paused for the entire duration specified in the - // header, yet this operation not having to pay the timeout cost in addition to that. - await delay(retrySec * 1000); - // resume the request queue and throw a non-abort error to signal a retry - this.requestQueue.start(); - // TODO: turn this into a WebAPIPlatformError - throw Error('A rate limit was exceeded.'); - } else { - throw new pRetry.AbortError(new Error('Retry header did not contain a valid timeout.')); } - } - // Slack's Web API doesn't use meaningful status codes besides 429 and 200 - if (response.statusCode !== 200) { - throw httpErrorFromResponse(response); - } + // Slack's Web API doesn't use meaningful status codes besides 429 and 200 + if (response.statusCode !== 200) { + throw httpErrorFromResponse(response); + } - result = this.buildResult(response); + result = this.buildResult(response); - // log warnings in response metadata - if (result.response_metadata !== undefined && result.response_metadata.warnings !== undefined) { - result.response_metadata.warnings.forEach(this.logger.warn); - } + // log warnings in response metadata + if (result.response_metadata !== undefined && result.response_metadata.warnings !== undefined) { + result.response_metadata.warnings.forEach(this.logger.warn); + } - if (!result.ok) { - const error = errorWithCode( - new Error(`An API error occurred: ${result.error}`), - ErrorCode.PlatformError, - ); - error.data = result; - throw new pRetry.AbortError(error); - } + if (!result.ok) { + const error = errorWithCode( + new Error(`An API error occurred: ${result.error}`), + ErrorCode.PlatformError, + ); + error.data = result; + throw new pRetry.AbortError(error); + } - return result; - } catch (error) { - this.logger.debug('http request failed'); - if (error.name === 'RequestError') { - throw requestErrorWithOriginal(error); - } else if (error.name === 'ReadError') { - throw readErrorWithOriginal(error); + return result; + } catch (error) { + this.logger.debug('http request failed'); + if (error.name === 'RequestError') { + throw requestErrorWithOriginal(error); + } else if (error.name === 'ReadError') { + throw readErrorWithOriginal(error); + } + throw error; } - throw error; - } - }; + }, + ); result = await pRetry(task, this.retryConfig); yield result; From 5138a8b1e0678c041081ff6a0376dfa0d43f3086 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 6 Aug 2018 17:50:06 -0700 Subject: [PATCH 5/5] update docs for rejectRateLimitedCalls --- docs/_pages/web_client.md | 46 +++++++++++++++++-- support/jsdoc/@slack-client-dist-WebClient.js | 13 ++++++ support/jsdoc/@slack-client.js | 2 + 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 support/jsdoc/@slack-client-dist-WebClient.js diff --git a/docs/_pages/web_client.md b/docs/_pages/web_client.md index 7642fe95f..45c2d0e0c 100644 --- a/docs/_pages/web_client.md +++ b/docs/_pages/web_client.md @@ -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); + } + }); +} ``` --- diff --git a/support/jsdoc/@slack-client-dist-WebClient.js b/support/jsdoc/@slack-client-dist-WebClient.js new file mode 100644 index 000000000..4c4a5831c --- /dev/null +++ b/support/jsdoc/@slack-client-dist-WebClient.js @@ -0,0 +1,13 @@ +/** + * @module @slack/client/dist/WebClient + */ + +/** + * @interface module:@slack/client/dist/WebClient.WebAPIRateLimitedError + * @extends module:@slack/client.CodedError + * @property {"slackclient_rate_limited_error"} code + * @property {number} retryAfter + */ +export class WebAPIRateLimitedError { +} + diff --git a/support/jsdoc/@slack-client.js b/support/jsdoc/@slack-client.js index 92fd6fba9..b701d0ed9 100644 --- a/support/jsdoc/@slack-client.js +++ b/support/jsdoc/@slack-client.js @@ -10,6 +10,7 @@ * @property ReadError * @property HTTPError * @property PlatformError + * @property RateLimitedError * @property RTMSendWhileDisconnectedError * @property RTMSendWhileNotReadyError * @property RTMSendMessagePlatformError @@ -409,6 +410,7 @@ export class WebClient { * @property {"undefined" | "undefined" | module:http.Agent | module:@slack/client/dist/util.__type} [agent] * @property {module:@slack/client.TLSOptions} [tls] * @property {number} [pageSize] + * @property {boolean} [rejectRateLimitedCalls] */ export class WebClientOptions { }