mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-19 13:03:31 +01:00
feat(Rest): optional ratelimit errors (#5659)
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
This commit is contained in:
@@ -492,6 +492,12 @@ class Client extends BaseClient {
|
|||||||
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
|
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
|
||||||
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
|
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
typeof options.rejectOnRateLimit !== 'undefined' &&
|
||||||
|
!(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit))
|
||||||
|
) {
|
||||||
|
throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
BaseManager: require('./managers/BaseManager'),
|
BaseManager: require('./managers/BaseManager'),
|
||||||
DiscordAPIError: require('./rest/DiscordAPIError'),
|
DiscordAPIError: require('./rest/DiscordAPIError'),
|
||||||
HTTPError: require('./rest/HTTPError'),
|
HTTPError: require('./rest/HTTPError'),
|
||||||
|
RateLimitError: require('./rest/RateLimitError'),
|
||||||
MessageFlags: require('./util/MessageFlags'),
|
MessageFlags: require('./util/MessageFlags'),
|
||||||
Intents: require('./util/Intents'),
|
Intents: require('./util/Intents'),
|
||||||
Permissions: require('./util/Permissions'),
|
Permissions: require('./util/Permissions'),
|
||||||
|
|||||||
55
src/rest/RateLimitError.js
Normal file
55
src/rest/RateLimitError.js
Normal file
@@ -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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time until this rate limit ends, in ms
|
||||||
|
* @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 maximum amount of requests of this end point
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RateLimitError;
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
const { AsyncQueue } = require('@sapphire/async-queue');
|
const { AsyncQueue } = require('@sapphire/async-queue');
|
||||||
const DiscordAPIError = require('./DiscordAPIError');
|
const DiscordAPIError = require('./DiscordAPIError');
|
||||||
const HTTPError = require('./HTTPError');
|
const HTTPError = require('./HTTPError');
|
||||||
|
const RateLimitError = require('./RateLimitError');
|
||||||
const {
|
const {
|
||||||
Events: { RATE_LIMIT, INVALID_REQUEST_WARNING },
|
Events: { RATE_LIMIT, INVALID_REQUEST_WARNING },
|
||||||
} = require('../util/Constants');
|
} = require('../util/Constants');
|
||||||
@@ -77,6 +78,30 @@ class RequestHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Determines whether the request should be queued or whether a RateLimitError should be thrown
|
||||||
|
*/
|
||||||
|
async onRateLimit(request, limit, timeout, isGlobal) {
|
||||||
|
const { options } = this.manager.client;
|
||||||
|
if (!options.rejectOnRateLimit) return;
|
||||||
|
|
||||||
|
const rateLimitData = {
|
||||||
|
timeout,
|
||||||
|
limit,
|
||||||
|
method: request.method,
|
||||||
|
path: request.path,
|
||||||
|
route: request.route,
|
||||||
|
global: isGlobal,
|
||||||
|
};
|
||||||
|
const shouldThrow =
|
||||||
|
typeof options.rejectOnRateLimit === 'function'
|
||||||
|
? await options.rejectOnRateLimit(rateLimitData)
|
||||||
|
: options.rejectOnRateLimit.some(route => rateLimitData.route.startsWith(route.toLowerCase()));
|
||||||
|
if (shouldThrow) {
|
||||||
|
throw new RateLimitError(rateLimitData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async execute(request) {
|
async execute(request) {
|
||||||
/*
|
/*
|
||||||
* After calculations have been done, pre-emptively stop further requests
|
* After calculations have been done, pre-emptively stop further requests
|
||||||
@@ -90,17 +115,10 @@ class RequestHandler {
|
|||||||
// Set the variables based on the global rate limit
|
// Set the variables based on the global rate limit
|
||||||
limit = this.manager.globalLimit;
|
limit = this.manager.globalLimit;
|
||||||
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
|
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
|
||||||
// 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
|
|
||||||
this.manager.globalDelay = this.globalDelayFor(timeout);
|
|
||||||
}
|
|
||||||
delayPromise = this.manager.globalDelay;
|
|
||||||
} else {
|
} else {
|
||||||
// Set the variables based on the route-specific rate limit
|
// Set the variables based on the route-specific rate limit
|
||||||
limit = this.limit;
|
limit = this.limit;
|
||||||
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
|
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
|
||||||
delayPromise = Util.delayFor(timeout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.manager.client.listenerCount(RATE_LIMIT)) {
|
if (this.manager.client.listenerCount(RATE_LIMIT)) {
|
||||||
@@ -125,6 +143,20 @@ class RequestHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (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
|
||||||
|
this.manager.globalDelay = this.globalDelayFor(timeout);
|
||||||
|
}
|
||||||
|
delayPromise = this.manager.globalDelay;
|
||||||
|
} else {
|
||||||
|
delayPromise = Util.delayFor(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether a RateLimitError should be thrown
|
||||||
|
await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
|
||||||
|
|
||||||
// Wait for the timeout to expire in order to avoid an actual 429
|
// Wait for the timeout to expire in order to avoid an actual 429
|
||||||
await delayPromise; // eslint-disable-line no-await-in-loop
|
await delayPromise; // eslint-disable-line no-await-in-loop
|
||||||
}
|
}
|
||||||
@@ -225,6 +257,20 @@ class RequestHandler {
|
|||||||
if (res.status === 429) {
|
if (res.status === 429) {
|
||||||
// A ratelimit was hit - this should never happen
|
// A ratelimit was hit - this should never happen
|
||||||
this.manager.client.emit('debug', `429 hit on route ${request.route}${sublimitTimeout ? ' for sublimit' : ''}`);
|
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();
|
||||||
|
}
|
||||||
|
await 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 caused by a sublimit, wait it out here so other requests on the route can be handled
|
||||||
if (sublimitTimeout) {
|
if (sublimitTimeout) {
|
||||||
await Util.delayFor(sublimitTimeout);
|
await Util.delayFor(sublimitTimeout);
|
||||||
|
|||||||
@@ -3,6 +3,24 @@
|
|||||||
const Package = (exports.Package = require('../../package.json'));
|
const Package = (exports.Package = require('../../package.json'));
|
||||||
const { Error, RangeError } = require('../errors');
|
const { Error, RangeError } = require('../errors');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limit data
|
||||||
|
* @typedef {Object} RateLimitData
|
||||||
|
* @property {number} timeout Time until this rate limit ends, in ms
|
||||||
|
* @property {number} limit The maximum amount of requests of this endpoint
|
||||||
|
* @property {string} method The http method of this request
|
||||||
|
* @property {string} path The path of the request relative to the HTTP endpoint
|
||||||
|
* @property {string} route The route of the request relative to the HTTP endpoint
|
||||||
|
* @property {boolean} global Whether this is a global rate limit
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this rate limit should throw an Error
|
||||||
|
* @typedef {Function} RateLimitQueueFilter
|
||||||
|
* @param {RateLimitData} rateLimitData The data of this rate limit
|
||||||
|
* @returns {boolean|Promise<boolean>}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for a client.
|
* Options for a client.
|
||||||
* @typedef {Object} ClientOptions
|
* @typedef {Object} ClientOptions
|
||||||
@@ -34,6 +52,10 @@ const { Error, RangeError } = require('../errors');
|
|||||||
* (or 0 for never)
|
* (or 0 for never)
|
||||||
* @property {number} [restGlobalRateLimit=0] How many requests to allow sending per second (0 for unlimited, 50 for
|
* @property {number} [restGlobalRateLimit=0] How many requests to allow sending per second (0 for unlimited, 50 for
|
||||||
* the standard global limit used by Discord)
|
* the standard global limit used by Discord)
|
||||||
|
* @property {string[]|RateLimitQueueFilter} [rejectOnRateLimit] Decides how rate limits and pre-emptive throttles
|
||||||
|
* should be handled. If this option is an array containing the prefix of the request route (e.g. /channels to match any
|
||||||
|
* route starting with /channels, such as /channels/222197033908436994/messages) or a function returning true, a
|
||||||
|
* {@link RateLimitError} will be thrown. Otherwise the request will be queued for later
|
||||||
* @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries)
|
* @property {number} [retryLimit=1] How many times to retry on 5XX errors (Infinity for indefinite amount of retries)
|
||||||
* @property {PresenceData} [presence={}] Presence data to use upon login
|
* @property {PresenceData} [presence={}] Presence data to use upon login
|
||||||
* @property {IntentsResolvable} intents Intents to enable for this connection
|
* @property {IntentsResolvable} intents Intents to enable for this connection
|
||||||
|
|||||||
8
typings/index.d.ts
vendored
8
typings/index.d.ts
vendored
@@ -1081,6 +1081,13 @@ declare module 'discord.js' {
|
|||||||
public requestData: HTTPErrorData;
|
public requestData: HTTPErrorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-empty-interface - Merge RateLimitData into RateLimitError to not have to type it again
|
||||||
|
interface RateLimitError extends RateLimitData {}
|
||||||
|
export class RateLimitError extends Error {
|
||||||
|
constructor(data: RateLimitData);
|
||||||
|
public name: 'RateLimitError';
|
||||||
|
}
|
||||||
|
|
||||||
export class Integration extends Base {
|
export class Integration extends Base {
|
||||||
constructor(client: Client, data: unknown, guild: Guild);
|
constructor(client: Client, data: unknown, guild: Guild);
|
||||||
public account: IntegrationAccount;
|
public account: IntegrationAccount;
|
||||||
@@ -2875,6 +2882,7 @@ declare module 'discord.js' {
|
|||||||
intents: BitFieldResolvable<IntentsString, number>;
|
intents: BitFieldResolvable<IntentsString, number>;
|
||||||
ws?: WebSocketOptions;
|
ws?: WebSocketOptions;
|
||||||
http?: HTTPOptions;
|
http?: HTTPOptions;
|
||||||
|
rejectOnRateLimit?: string[] | ((data: RateLimitData) => boolean | Promise<boolean>);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientPresenceStatus = 'online' | 'idle' | 'dnd';
|
type ClientPresenceStatus = 'online' | 'idle' | 'dnd';
|
||||||
|
|||||||
Reference in New Issue
Block a user