Skip to content

Commit 25aff7e

Browse files
didineleJiralitevladfrangu
authored
feat(GuildMemberManager): handle gateway request rate limit (#11252)
* feat(GuildMemberManager): handle gateway request ratelimit * chore: typo no one saw * fix: cleanup listener properly * types: add error code * refactor: requested changes * fix: update emitted warning * chore: requested changes * refactor: remove event * refactor: warning * chore: wording Co-authored-by: Vlad Frangu <[email protected]> --------- Co-authored-by: Jiralite <[email protected]> Co-authored-by: Vlad Frangu <[email protected]>
1 parent 9f18cb2 commit 25aff7e

File tree

3 files changed

+69
-23
lines changed

3 files changed

+69
-23
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 = (client, { 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+
`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.`,
18+
);
19+
20+
emittedFor.add(data.opcode);
21+
}
22+
}
23+
}
24+
};

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 PacketHandlers = Object.fromEntries([
5252
['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI.js')],
5353
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE.js')],
5454
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE.js')],
55+
['RATE_LIMITED', require('./RATE_LIMITED.js')],
5556
['READY', require('./READY.js')],
5657
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS.js')],
5758
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE.js')],

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

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
const { setTimeout, clearTimeout } = require('node:timers');
44
const { Collection } = require('@discordjs/collection');
55
const { makeURLSearchParams } = require('@discordjs/rest');
6+
const { GatewayRateLimitError } = require('@discordjs/util');
7+
const { WebSocketShardEvents } = require('@discordjs/ws');
68
const { DiscordSnowflake } = require('@sapphire/snowflake');
7-
const { Routes, GatewayOpcodes } = require('discord-api-types/v10');
9+
const { Routes, GatewayOpcodes, GatewayDispatchEvents } = require('discord-api-types/v10');
810
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors/index.js');
911
const { BaseGuildVoiceChannel } = require('../structures/BaseGuildVoiceChannel.js');
1012
const { GuildMember } = require('../structures/GuildMember.js');
@@ -246,46 +248,65 @@ class GuildMemberManager extends CachedManager {
246248
const query = initialQuery ?? (users ? undefined : '');
247249

248250
return new Promise((resolve, reject) => {
249-
this.guild.client.ws.send(this.guild.shardId, {
250-
op: GatewayOpcodes.RequestGuildMembers,
251-
// eslint-disable-next-line id-length
252-
d: {
253-
guild_id: this.guild.id,
254-
presences,
255-
user_ids: users,
256-
query,
257-
nonce,
258-
limit,
259-
},
260-
});
261251
const fetchedMembers = new Collection();
262252
let index = 0;
253+
254+
const cleanup = () => {
255+
/* eslint-disable no-use-before-define */
256+
clearTimeout(timeout);
257+
258+
this.client.ws.removeListener(WebSocketShardEvents.Dispatch, rateLimitHandler);
259+
this.client.removeListener(Events.GuildMembersChunk, handler);
260+
this.client.decrementMaxListeners();
261+
/* eslint-enable no-use-before-define */
262+
};
263+
264+
const timeout = setTimeout(() => {
265+
cleanup();
266+
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
267+
}, time).unref();
268+
263269
const handler = (members, _, chunk) => {
264270
if (chunk.nonce !== nonce) return;
265271

266-
// eslint-disable-next-line no-use-before-define
267272
timeout.refresh();
268273
index++;
269274
for (const member of members.values()) {
270275
fetchedMembers.set(member.id, member);
271276
}
272277

273278
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || index === chunk.count) {
274-
// eslint-disable-next-line no-use-before-define
275-
clearTimeout(timeout);
276-
this.client.removeListener(Events.GuildMembersChunk, handler);
277-
this.client.decrementMaxListeners();
279+
cleanup();
278280
resolve(users && !Array.isArray(users) && fetchedMembers.size ? fetchedMembers.first() : fetchedMembers);
279281
}
280282
};
281283

282-
const timeout = setTimeout(() => {
283-
this.client.removeListener(Events.GuildMembersChunk, handler);
284-
this.client.decrementMaxListeners();
285-
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
286-
}, time).unref();
284+
const requestData = {
285+
guild_id: this.guild.id,
286+
presences,
287+
user_ids: users,
288+
query,
289+
nonce,
290+
limit,
291+
};
292+
293+
const rateLimitHandler = payload => {
294+
if (payload.t === GatewayDispatchEvents.RateLimited && payload.d.meta.nonce === nonce) {
295+
cleanup();
296+
reject(new GatewayRateLimitError(payload.d, requestData));
297+
}
298+
};
299+
300+
this.client.ws.on(WebSocketShardEvents.Dispatch, rateLimitHandler);
301+
287302
this.client.incrementMaxListeners();
288303
this.client.on(Events.GuildMembersChunk, handler);
304+
305+
this.guild.client.ws.send(this.guild.shardId, {
306+
op: GatewayOpcodes.RequestGuildMembers,
307+
// eslint-disable-next-line id-length
308+
d: requestData,
309+
});
289310
});
290311
}
291312

0 commit comments

Comments
 (0)