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

feat(Rest): optional ratelimit errors #5659

Merged
merged 13 commits into from Jun 9, 2021
Merged
6 changes: 6 additions & 0 deletions src/client/Client.js
Expand Up @@ -492,6 +492,12 @@ class Client extends BaseClient {
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
}
if (
typeof options.disableRateLimitQueue !== 'undefined' &&
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
!(typeof options.disableRateLimitQueue === 'function' || Array.isArray(options.disableRateLimitQueue))
) {
throw new TypeError('CLIENT_INVALID_OPTION', 'disableRateLimitQueue', 'an array or a function');
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -21,6 +21,7 @@ module.exports = {
BaseManager: require('./managers/BaseManager'),
DiscordAPIError: require('./rest/DiscordAPIError'),
HTTPError: require('./rest/HTTPError'),
RateLimitError: require('./rest/RateLimitError'),
MessageFlags: require('./util/MessageFlags'),
Intents: require('./util/Intents'),
Permissions: require('./util/Permissions'),
Expand Down
55 changes: 55 additions & 0 deletions src/rest/RateLimitError.js
@@ -0,0 +1,55 @@
'use strict';

/**
* Represents a RateLimit error from a request.
* @extends Error
*/
class RateLimitError extends Error {
constructor({ timeout, limit, method, path, route, global }) {
super(`A ${global ? 'global ' : ''}rate limit was hit on route ${route}`);

/**
* The name of the error
* @type {string}
*/
this.name = 'RateLimitError';

/**
* HTTP error code returned from the request
* @type {number}
*/
this.timeout = timeout;

/**
* The HTTP method used for the request
* @type {string}
*/
this.method = method;

/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = path;

/**
* The route of the request relative to the HTTP endpoint
* @type {string}
*/
this.route = route;

/**
* Whether this rate limit is global
* @type {boolean}
*/
this.global = global;

/**
* The rate limit of this endpoint
* @type {number}
*/
this.limit = limit;
}
}

module.exports = RateLimitError;
45 changes: 44 additions & 1 deletion src/rest/RequestHandler.js
Expand Up @@ -3,6 +3,7 @@
const AsyncQueue = require('./AsyncQueue');
const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const RateLimitError = require('./RateLimitError');
const {
Events: { RATE_LIMIT, INVALID_REQUEST_WARNING },
} = require('../util/Constants');
Expand Down Expand Up @@ -77,6 +78,28 @@ class RequestHandler {
});
}

onRateLimit(request, limit, timeout, isGlobal) {
if (this.manager.client.options.disableRateLimitQueue) {
const rateLimitData = {
timeout,
limit,
method: request.method,
path: request.path,
route: request.route,
global: isGlobal,
};
const shouldThrow =
Vendicated marked this conversation as resolved.
Show resolved Hide resolved
typeof this.manager.client.options.disableRateLimitQueue === 'function'
? this.manager.client.options.disableRateLimitQueue(rateLimitData)
: this.manager.client.options.disableRateLimitQueue.some(route =>
rateLimitData.route.startsWith(`/${route}/`),
);
if (shouldThrow) {
throw new RateLimitError(rateLimitData);
}
}
}

async execute(request) {
/*
* After calculations have been done, pre-emptively stop further requests
Expand All @@ -90,6 +113,9 @@ class RequestHandler {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();

this.onRateLimit(request, limit, timeout, isGlobal);

// If this is the first task to reach the global timeout, set the global delay
if (!this.manager.globalDelay) {
// The global delay function should clear the global delay state when it is resolved
Expand All @@ -100,6 +126,9 @@ class RequestHandler {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();

this.onRateLimit(request, limit, timeout, isGlobal);

delayPromise = Util.delayFor(timeout);
}

Expand Down Expand Up @@ -223,8 +252,22 @@ class RequestHandler {
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
// A ratelimit was hit - this should never happen
// A ratelimit was hit - this should only happen if client is already rate limited when connecting
Vendicated marked this conversation as resolved.
Show resolved Hide resolved
this.manager.client.emit('debug', `429 hit on route ${request.route}${sublimitTimeout ? ' for sublimit' : ''}`);

const isGlobal = this.globalLimited;
let limit, timeout;
if (isGlobal) {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
} else {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
}
this.onRateLimit(request, limit, timeout, isGlobal);

// If caused by a sublimit, wait it out here so other requests on the route can be handled
if (sublimitTimeout) {
await Util.delayFor(sublimitTimeout);
Expand Down
12 changes: 9 additions & 3 deletions typings/index.d.ts
Expand Up @@ -461,9 +461,7 @@ declare module 'discord.js' {
Endpoints: {
botGateway: string;
invite: (root: string, code: string) => string;
CDN: (
root: string,
) => {
CDN: (root: string) => {
Asset: (name: string) => string;
DefaultAvatar: (id: string | number) => string;
Emoji: (emojiID: string, format: 'png' | 'gif') => string;
Expand Down Expand Up @@ -2659,6 +2657,7 @@ declare module 'discord.js' {
intents: BitFieldResolvable<IntentsString, number>;
ws?: WebSocketOptions;
http?: HTTPOptions;
disableRateLimitQueue?: string[] | ((data: RateLimitData) => boolean);
}

type ClientPresenceStatus = 'online' | 'idle' | 'dnd';
Expand Down Expand Up @@ -3489,6 +3488,13 @@ declare module 'discord.js' {
global: boolean;
}

// tslint:disable-next-line:no-empty-interface - Merge RateLimitData interface to not have to type it again
interface RateLimitError extends RateLimitData {}
export class RateLimitError extends Error {
iCrawl marked this conversation as resolved.
Show resolved Hide resolved
constructor(data: RateLimitData);
public name: 'RateLimitError';
}

interface InvalidRequestWarningData {
count: number;
remainingTime: number;
Expand Down