Skip to content

Commit a3c6bce

Browse files
didineleJiralite
andauthored
feat(GuildMemberManager): handle gateway request rate limit (#11253)
* feat(GuildMemberManager): handle gateway request ratelimit * fix: account for ws abstraction layer * chore: yada yada * refactor: use new error class * fix: linter --------- Co-authored-by: Jiralite <[email protected]>
1 parent 214c6cb commit a3c6bce

File tree

3 files changed

+71
-20
lines changed

3 files changed

+71
-20
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
const process = require('node:process');
4+
const { GatewayOpcodes } = require('discord-api-types/v10');
5+
6+
const emittedFor = new Set();
7+
8+
module.exports = (_, { d: data }) => {
9+
switch (data.opcode) {
10+
case GatewayOpcodes.RequestGuildMembers: {
11+
break;
12+
}
13+
14+
default: {
15+
if (!emittedFor.has(data.opcode)) {
16+
process.emitWarning(
17+
// eslint-disable-next-line max-len
18+
`Hit a gateway rate limit on opcode ${data.opcode} (${GatewayOpcodes[data.opcode]}). If the discord.js version you're using is up-to-date, please open an issue on GitHub.`,
19+
);
20+
21+
emittedFor.add(data.opcode);
22+
}
23+
}
24+
}
25+
};

packages/discord.js/src/client/websocket/handlers/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const handlers = Object.fromEntries([
5252
['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')],
5353
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')],
5454
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')],
55+
['RATE_LIMITED', require('./RATE_LIMITED')],
5556
['READY', require('./READY')],
5657
['RESUMED', require('./RESUMED')],
5758
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS')],

packages/discord.js/src/managers/GuildMemberManager.js

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ const { process } = require('node:process');
44
const { setTimeout, clearTimeout } = require('node:timers');
55
const { Collection } = require('@discordjs/collection');
66
const { makeURLSearchParams } = require('@discordjs/rest');
7+
const { GatewayRateLimitError } = require('@discordjs/util');
78
const { DiscordSnowflake } = require('@sapphire/snowflake');
8-
const { Routes, GatewayOpcodes } = require('discord-api-types/v10');
9+
const { Routes, GatewayOpcodes, GatewayDispatchEvents } = require('discord-api-types/v10');
910
const CachedManager = require('./CachedManager');
1011
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors');
1112
const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
@@ -239,19 +240,25 @@ class GuildMemberManager extends CachedManager {
239240

240241
return new Promise((resolve, reject) => {
241242
if (!query && !users) query = '';
242-
this.guild.shard.send({
243-
op: GatewayOpcodes.RequestGuildMembers,
244-
d: {
245-
guild_id: this.guild.id,
246-
presences,
247-
user_ids: users,
248-
query,
249-
nonce,
250-
limit,
251-
},
252-
});
253243
const fetchedMembers = new Collection();
254244
let i = 0;
245+
246+
const cleanup = () => {
247+
/* eslint-disable no-use-before-define */
248+
clearTimeout(timeout);
249+
250+
this.client.removeListener(Events.Raw, rateLimitHandler);
251+
this.client.decrementMaxListeners();
252+
this.client.removeListener(Events.GuildMembersChunk, handler);
253+
this.client.decrementMaxListeners();
254+
/* eslint-enable no-use-before-define */
255+
};
256+
257+
const timeout = setTimeout(() => {
258+
cleanup();
259+
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
260+
}, time).unref();
261+
255262
const handler = (members, _, chunk) => {
256263
if (chunk.nonce !== nonce) return;
257264
timeout.refresh();
@@ -260,19 +267,37 @@ class GuildMemberManager extends CachedManager {
260267
fetchedMembers.set(member.id, member);
261268
}
262269
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
263-
clearTimeout(timeout);
264-
this.client.removeListener(Events.GuildMembersChunk, handler);
265-
this.client.decrementMaxListeners();
270+
cleanup();
266271
resolve(users && !Array.isArray(users) && fetchedMembers.size ? fetchedMembers.first() : fetchedMembers);
267272
}
268273
};
269-
const timeout = setTimeout(() => {
270-
this.client.removeListener(Events.GuildMembersChunk, handler);
271-
this.client.decrementMaxListeners();
272-
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
273-
}, time).unref();
274+
275+
const requestData = {
276+
guild_id: this.guild.id,
277+
presences,
278+
user_ids: users,
279+
query,
280+
nonce,
281+
limit,
282+
};
283+
284+
const rateLimitHandler = payload => {
285+
if (payload.t === GatewayDispatchEvents.RateLimited && payload.d.meta.nonce === nonce) {
286+
cleanup();
287+
reject(new GatewayRateLimitError(payload.d, requestData));
288+
}
289+
};
290+
291+
this.client.incrementMaxListeners();
292+
this.client.on(Events.Raw, rateLimitHandler);
293+
274294
this.client.incrementMaxListeners();
275295
this.client.on(Events.GuildMembersChunk, handler);
296+
297+
this.guild.shard.send({
298+
op: GatewayOpcodes.RequestGuildMembers,
299+
d: requestData,
300+
});
276301
});
277302
}
278303

0 commit comments

Comments
 (0)