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

Bot send embed messages twice or even triple times after being not used for a while #10301

Open
wisienak opened this issue May 26, 2024 · 15 comments

Comments

@wisienak
Copy link

wisienak commented May 26, 2024

Which package is this bug report for?

discord.js

Issue description

After bot being not used for example 2 hours, after firing (joining from another account) to the discord server, bot send embed message twice. And it only appears when the bot is not used for a while, when i work with bot and restart him often, this problem doesn't appear.

It it matter bot is in screen everytime. No any errors in output.

Code sample

client.on('guildMemberAdd', (member) => {
    const attachment = new AttachmentBuilder('images/welcome.png', { name: 'welcome.png' });
    const embed = new EmbedBuilder()
        .setColor(0x169C9C)
        .setDescription(`Welcolme <@${member.user.id}>! (${member.user.username})\nSome text!\nSome text.\nSome text:\n<#${process.env.RULES_CHANNEL}>\n<#${process.env.TICKETS_CHANNEL}>`)
        .setThumbnail(`https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`)
        .setImage('attachment://welcome.png');
    
    const channel = member.guild.channels.cache.get(process.env.WELCOME_CHANNEL);
    channel.send({ embeds: [embed], files: [attachment] });
});

Versions

node.js version: v20.13.1
discord.js version: 14.15.2

OS version: Ubuntu 20.04 LTS x86_64 (this problem also appears on my PC when i host it locally)

Issue priority

High (immediate attention needed)

Which partials do you have configured?

No Partials

Which gateway intents are you subscribing to?

Guilds, GuildMembers, GuildMessages, GuildMessageReactions, MessageContent

I have tested this issue on a development release

No response

@wisienak
Copy link
Author

wisienak commented May 26, 2024

During my observation

I added to the event a console.log message to debug how many this event is firing, but i only got one log but 3 messages on channel. Where can be problem?

@almostSouji
Copy link
Member

after that time, does the event always fire X times?
if so, that sounds like the code attaching a listener is called multiple times over the session, which would cause the event callback to be execute X times, where X is the amount of listeners attached.
you can check for that with console.log(client.listenerCount("guildMemberAdd"))

@wisienak
Copy link
Author

after that time, does the event always fire X times? if so, that sounds like the code attaching a listener is called multiple times over the session, which would cause the event callback to be execute X times, where X is the amount of listeners attached. you can check for that with console.log(client.listenerCount("guildMemberAdd"))

everytime event got fired 1 time, and this method shows 1 after debug

@kyranet
Copy link
Member

kyranet commented May 27, 2024

For reference, discord.js retries requests (by default, up to 3 times 1):

res = await manager.options.makeRequest(url, { ...options, signal: controller.signal });
} catch (error: unknown) {
if (!(error instanceof Error)) throw error;
// Retry the specified number of times if needed
if (shouldRetry(error) && retries !== manager.options.retries) {
// Retry is handled by the handler upon receiving null
return null;
}

But only on timeout (by default 15 seconds2) and ECONNRESET3:

/**
* Check whether an error indicates that a retry can be attempted
*
* @param error - The error thrown by the network request
* @returns Whether the error indicates a retry should be attempted
*/
export function shouldRetry(error: Error | NodeJS.ErrnoException) {
// Retry for possible timed out requests
if (error.name === 'AbortError') return true;
// Downlevel ECONNRESET to retry as it may be recoverable
return ('code' in error && error.code === 'ECONNRESET') || error.message.includes('ECONNRESET');
}

Since ECONNRESET implies Discord didn't process the request, this can then only mean a timeout, specifically, that your application has sent the request correctly, but due to Internet's nature, the response can fail make its way back to your server, making your app unable acknowledge the response, timing out as a result.

To work around this, we have two options:

Setting options.rest.timeout to change the amount of time discord.js will wait before attempting a retry. This may increase (slightly) the chances of receiving a request, but it will also block any subsequent requests during the duration. For 3 retries at 15 seconds, a request can block for as long as 45 seconds.

Another solution, released by Discord rather recently, is to set enforceNonce to true and generate a random nonce. The combination of the two fields allows Discord to deduplicate the request and therefore even if the library successfully sent 3 requests, Discord would take the first and ignore the rest, sending only one message.

To generate a random nonce reliably, you can use SnowflakeUtil:

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

Footnotes

  1. https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/constants.ts#L24

  2. https://github.com/discordjs/discord.js/blob/d22b55fc829226fbfded9c38e7d33160efce67ea/packages/rest/src/lib/utils/constants.ts#L25

  3. The ECONRESET error means that the server unexpectedly closed the connection and the request to the server was not fulfilled.

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

For reference, discord.js retries requests (by default, up to 3 times 1):

res = await manager.options.makeRequest(url, { ...options, signal: controller.signal });
} catch (error: unknown) {
if (!(error instanceof Error)) throw error;
// Retry the specified number of times if needed
if (shouldRetry(error) && retries !== manager.options.retries) {
// Retry is handled by the handler upon receiving null
return null;
}

But only on timeout (by default 15 seconds2) and ECONNRESET3:

/**
* Check whether an error indicates that a retry can be attempted
*
* @param error - The error thrown by the network request
* @returns Whether the error indicates a retry should be attempted
*/
export function shouldRetry(error: Error | NodeJS.ErrnoException) {
// Retry for possible timed out requests
if (error.name === 'AbortError') return true;
// Downlevel ECONNRESET to retry as it may be recoverable
return ('code' in error && error.code === 'ECONNRESET') || error.message.includes('ECONNRESET');
}

Since ECONNRESET implies Discord didn't process the request, this can then only mean a timeout, specifically, that your application has sent the request correctly, but due to Internet's nature, the response can fail make its way back to your server, making your app unable acknowledge the response, timing out as a result.

To work around this, we have two options:

Setting options.rest.timeout to change the amount of time discord.js will wait before attempting a retry. This may increase (slightly) the chances of receiving a request, but it will also block any subsequent requests during the duration. For 3 retries at 15 seconds, a request can block for as long as 45 seconds.

Another solution, released by Discord rather recently, is to set enforceNonce to true and generate a random nonce. The combination of the two fields allows Discord to deduplicate the request and therefore even if the library successfully sent 3 requests, Discord would take the first and ignore the rest, sending only one message.

To generate a random nonce reliably, you can use SnowflakeUtil:

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

Footnotes

  1. The ECONRESET error means that the server unexpectedly closed the connection and the request to the server was not fulfilled.

After add snowflakeutil

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

this problem still appears

@Jiralite
Copy link
Member

Jiralite commented Jun 3, 2024

Show how you implemented this nonce into your code please.

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

like this

import { SnowflakeUtil } from 'discord.js';

const nonce = SnowflakeUtil.generate();

@Jiralite
Copy link
Member

Jiralite commented Jun 3, 2024

You said that already.

How did you implement that into your code? What did you do with that variable? Where did you put it? Etc.

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

i just use require to SnowflakeUtil and create this nonce variable with generate method inside index.js thats it

@tipakA
Copy link
Contributor

tipakA commented Jun 3, 2024

So you aren't actually using that variable anywhere?

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

oh sorry, i read kyranet comment again and i did mistake, i need to enable enforceNonce and use nonce generated string to use this option recommended by discord.

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

ok so i implemented it like this

require('dotenv').config();
const { EmbedBuilder, AttachmentBuilder, SnowflakeUtil } = require('discord.js');

module.exports = (member) => {
    const attachment = new AttachmentBuilder('images/welcome.png', { name: 'welcome.png' });
    const embed = new EmbedBuilder()
        .setColor(0x169C9C)
        .setDescription(`Welcome<@${member.user.id}>! (${member.user.username})\nSome text!\nSome text.\nSome text:\n<#${process.env.RULES_CHANNEL}>\n<#${process.env.TICKETS_CHANNEL}>`)
        .setThumbnail(`https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`)
        .setImage('attachment://welcome.png');
    
    const nonce = SnowflakeUtil.generate();
    const channel = member.guild.channels.cache.get(process.env.WELCOME_CHANNEL);
    channel.send({ embeds: [embed], files: [attachment], enforceNonce: true, nonce: nonce });
};

i think it should work now

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

huh something dont work

/home/bot2/node_modules/discord.js/src/structures/MessagePayload.js:132
        throw new DiscordjsRangeError(ErrorCodes.MessageNonceType);
              ^

RangeError [MessageNonceType]: Message nonce must be an integer or a string.
}

@Qjuh
Copy link
Contributor

Qjuh commented Jun 3, 2024

Because SnowflakeUtil.generate() gives you a BigInt. You‘d need to .toString() it when passing in your send.

@wisienak
Copy link
Author

wisienak commented Jun 3, 2024

Because SnowflakeUtil.generate() gives you a BigInt. You‘d need to .toString() it when passing in your send.

yes i did it now, thanks for reply

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants