-
Notifications
You must be signed in to change notification settings - Fork 196
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
Looking for a proper way to handle connection errors. #92
Comments
Hi! Auto-reconnect should work in your case, but it does not handle the first initial connection, that is a classic request. You already handle possible errors (thrown by So in your catch block, if (error.code === 429) {
// restart stream
this.stream();
} |
Allow me to clarify something. I became aware of the fact that my example above is failing, and in addition to that I am also now aware that With all of that in mind, before I wrote this reply here, I had production code with const { ETwitterStreamEvent, TweetStream, TwitterApi, ETwitterApiError } = require('twitter-api-v2');
const { MessageActionRow, MessageButton } = require('discord.js');
const TwitterSources = require('../models/twitter-sources');
const Discord = require('../util/discord');
class Twitter {
stripAt(handle) {
return handle.replace(/^@/, '');
}
stripShorteners(text) {
return text.replace(/\shttps:\/\/t\.co\/[\S]{5,}$/gmi, '');
}
async getSourceRuleId(handle) {
const sourceList = await this.client.v2.streamRules();
console.log(sourceList);
return sourceList.data.find(source => source.tag.toLowerCase() === handle.toLowerCase()).id;
}
async listSources() {
const sourceList = await this.client.v2.streamRules();
if (sourceList.data) {
return sourceList.data
.map(source => `\`@${source.tag}\``)
.sort((a, b) => a.localeCompare(b))
.join(', ');
}
else {
return 'No entries found.';
}
}
async addSource(handle) {
handle = this.stripAt(handle);
await TwitterSources.findOneAndUpdate(
{ name: new RegExp(handle, 'i') },
{ name: handle },
{ upsert: true, runValidators: true }
);
await this.client.v2.updateStreamRules({
add: [
{
value: `(from:${handle}) -is:retweet -is:reply`,
tag: handle
}
]
});
return true;
}
async removeSource(handle) {
handle = this.stripAt(handle);
await TwitterSources.findOneAndDelete({ name: new RegExp(handle, 'i') });
await this.client.v2.updateStreamRules({
delete: { ids: [await this.getSourceRuleId(handle)] }
});
return true;
}
async initSources() {
const sourceList = await TwitterSources.find({}).lean();
const streamRules = await this.client.v2.streamRules();
if (streamRules?.data?.length > 0) {
await this.client.v2.updateStreamRules({
delete: {
ids: streamRules.data.map(rule => rule.id),
},
});
}
await this.client.v2.updateStreamRules({
add: sourceList.map(source => ({
value: `(from:${source.name}) -is:retweet -is:reply`,
tag: source.name
}))
});
}
embedTweet(data) {
return ({
description: data.text,
url: data.url,
color: 15329769,
timestamp: data.time,
footer: {
text: data.authorName,
icon_url: data.authorPhoto
},
...data.thumbnail && {
thumbnail: {
url: data.thumbnail
}
}
})
}
embedActions(data) {
return new MessageActionRow()
.addComponents(
new MessageButton({
label: 'Source',
style: 'LINK',
url: data.url,
}),
new MessageButton({
customId: 'POST_ON_REDDIT',
label: 'Post on Reddit',
style: 'SECONDARY',
emoji: '776887363901063189'
})
);
}
async stream() {
this.client = new TwitterApi(process.env.TWITTER_BEARER_TOKEN);
await this.initSources();
this.stream = await this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys']
}
);
this.stream.autoReconnect = true;
this.stream.autoReconnectRetries = 999;
this.stream.on(ETwitterStreamEvent.Data, async (tweet) => {
const author = tweet.includes.users.find(user => user.id === tweet.data.author_id);
const tweetData = {
id: tweet.data.id,
url: `https://twitter.com/${author.username}/status/${tweet.data.id}`,
authorName: author.username,
authorPhoto: author.profile_image_url,
text: this.stripShorteners(tweet.data.text),
time: new Date(tweet.data.created_at).getTime(),
...tweet?.includes?.media?.[0]?.url && {
thumbnail: tweet.includes.media[0].url
}
}
await Discord.channels.cache
.get(process.env.TWITTER_CHANNEL_ID)
.send({
embeds: [this.embedTweet(tweetData)],
components: [this.embedActions(tweetData)]
});
});
}
}
const twitter = new Twitter();
module.exports = twitter; Provided that there is no In any case, please don't consider responding to me a priority as I don't want to take too much of your time, I have working code now anyway so not a big deal. |
Hi, NOTE: If the given script is a copy-paste, be careful of the naming of your variables. You name a method
this.client = new TwitterApi(process.env.TWITTER_BEARER_TOKEN);
await this.initSources();
try {
this.stream = await this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys']
}
);
} catch (e) {
// await and reconnect?
this.startStream();
return;
}
this.stream.autoReconnect = true;
this.stream.autoReconnectRetries = Infinity;
// then attach event handlers... For handling connection and stream errors after the first call, you must use the following events:
Reconnections are fully automatic. If those events aren't triggered, then there is a problem elsewhere. How do you know in production when there is a connection error? At which step of your script does these errors occur? |
Hello once again @alkihis, thank you for your reply.
This was indeed a copy-paste from the code running on the server. And you have correctly pointed out that the class method of
So as mentioned, because Twitter does not allow you to run two or more concurrent connections (stream), I temporarily turned off my bot, then ran a modified version of the code on my local machine, and I can confirm that everything works as intended. Upon launching the script and then turning off my network connection to artificially simulate a disconnect, I've got the following output:
The event handlers are emitting properly, and the connection automatically recovered upon turning on my network again. Thanks so much once again for helping me, I appreciate it! |
Hello again, apologies for reopening this issue but I've ran into some unexpected problems, and I was hoping you can help me figure it out. Basically I have made the suggested adjustments to my code since we last talked, and pushed them to my production server for the Discord bot that I am running. This morning I found out that the bot has crashed because it ran out of memory. It appears that I've lost connection to the Twitter stream, and
As you can see, these events were emitted mere milliseconds between each other, and I had thousands of those entries in my log files. Since then I have turned off the bot and I was able to reproduce this same behavior on my local machine. Here is a full sample that you can also run: const { TwitterApi, ETwitterStreamEvent } = require('twitter-api-v2');
class Twitter {
async startStream() {
try {
this.client = new TwitterApi(TWITTER_BEARER_TOKEN);
this.stream = await this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys']
}
);
console.log('Connected to the Twitter stream');
this.stream.autoReconnect = true;
this.stream.autoReconnectRetries = Infinity;
this.stream.on(ETwitterStreamEvent.Data, async (tweet) => {
console.log(tweet);
});
this.stream.on(ETwitterStreamEvent.Error, async (error) => {
console.log(`Twitter Event:Error: ${JSON.stringify(error)}`);
});
this.stream.on(ETwitterStreamEvent.ReconnectAttempt, async () => {
console.log(`Twitter Event:ReconnectAttempt`);
});
this.stream.on(ETwitterStreamEvent.Reconnected, async () => {
console.log(`Twitter Event:Reconnected`);
});
this.stream.on(ETwitterStreamEvent.DataKeepAlive, async () => {
console.log(`Twitter Event:DataKeepAlive`);
});
} catch (error) {
console.log(error);
}
}
}
const twitter = new Twitter();
(async () => {
await twitter.startStream();
})(); To reproduce the disconnect and the subsequent flood of emits, I forcefully shut down the connections to On Linux, after I initially and successfully connect to the stream I run this command to find out which port it's running on - After a few seconds, it kills the connection and the events start emitting like crazy.
I am not exactly sure why that's happening, because a few days ago I ran some tests before pushing the code on my server, and the disconnects were handled exactly as documented, or in particular:
But in the example I provided the retries occurred milliseconds apart from one another, and as far as I can tell, the stream didn't even manage to recover. Do you have any idea why this might be happening? |
Hi again, I don't really know what's happening and why the reconnect timeout isn't awaited. EDIT: After a quick review, I know what's happening and it's all my fault 😅 Very sorry for the inconvenience. I'll update this issue when bugfix is available on npm. |
No inconvenience was caused at all, and if the report helped fix a bug than that makes me happy, so thank you very much for looking into it. I have also tested this on my end using the same I am not sure if I should close the issue or edit the title and leave it open, so whatever you suggest. Anyway, thanks once again for helping out! |
Hello again, 1.6.1 update that contains the fix is now available on npm :) async startStream() {
this.client = new TwitterApi(TWITTER_BEARER_TOKEN);
this.stream = this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys'],
'autoConnect': false, // Note the autoConnect: false
}
);
this.stream.on(ETwitterStreamEvent.Data, async (tweet) => {
console.log(tweet);
});
this.stream.on(ETwitterStreamEvent.Error, async (error) => {
console.log(`Twitter Event:Error: ${JSON.stringify(error)}`);
});
this.stream.on(ETwitterStreamEvent.ReconnectAttempt, async () => {
console.log(`Twitter Event:ReconnectAttempt`);
});
this.stream.on(ETwitterStreamEvent.Reconnected, async () => {
console.log(`Twitter Event:Reconnected`);
});
this.stream.on(ETwitterStreamEvent.DataKeepAlive, async () => {
console.log(`Twitter Event:DataKeepAlive`);
});
this.stream.on(ETwitterStreamEvent.Connected, async () => {
console.log('Connected to the Twitter stream');
});
try {
// Options are directly given in .connect() method
await this.stream.connect({ autoReconnect: true, autoReconnectRetries: Infinity });
} catch (error) {
console.log('Unable to establish the first connection. Auto-reconnect will be fired soon.');
console.log(error);
}
} If you want to configure the retry timeout computed between each reconnect attempt, you can now overwrite Keep me updated if the new version meets your expectations. :) |
Hey @alkihis how are you doing. Really appreciate all these updates (1.16.1), and as a matter of fact, I have already been using and testing them for the last 36-48 hours because I ran into some more connection issues, although they might and probably are not directly related to your library these new tools and abilities came extremely handy to me. I might my issues in another comment, but for now I just wanted to let you know that I have tested this functionality you described above, and in one specific case That specific case is an error 429: ApiResponseError: Request failed with code 429
at RequestHandlerHelper.createResponseError (/home/user/projects/barca_discord_bot/node_modules/twitter-api-v2/dist/client-mixins/request-handler.helper.js:70:16)
at RequestHandlerHelper.onResponseEndHandler (/home/user/projects/barca_discord_bot/node_modules/twitter-api-v2/dist/client-mixins/request-handler.helper.js:110:25)
at IncomingMessage.emit (node:events:406:35)
at endReadableNT (node:internal/streams/readable:1343:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21) {
error: true,
type: 'response',
code: 429,
headers: {
// some private header data removed here
date: 'Wed, 20 Oct 2021 22:01:38 UTC',
server: 'tsa_o',
'content-type': 'application/json; charset=utf-8',
'cache-control': 'no-cache, no-store, max-age=0',
'content-length': '213',
'x-access-level': 'read',
'x-frame-options': 'SAMEORIGIN',
'x-xss-protection': '0',
'x-rate-limit-limit': '50',
'x-rate-limit-reset': '1634767472',
'content-disposition': 'attachment; filename=json.json',
'x-content-type-options': 'nosniff',
'x-rate-limit-remaining': '16',
'strict-transport-security': 'max-age=631138519',
connection: 'close'
},
rateLimit: { limit: 50, remaining: 16, reset: 1634767472 },
data: {
title: 'ConnectionException',
detail: 'This stream is currently at the maximum allowed connection limit.',
connection_issue: 'TooManyConnections',
type: 'https://api.twitter.com/2/problems/streaming-connection'
}
} Here is a full reproducible script: const { ETwitterStreamEvent, TwitterApi } = require('twitter-api-v2');
const TWITTER_BEARER_TOKEN = 'YOUR_TOKEN';
/*
This is just a little trick to simiulate server enviornment,
and keep the process running forever, otherwise the process exits
after the script is executed.
*/
const server = require('http').createServer();
server.listen(12345);
class Twitter {
async startStream() {
try {
if (!this.client) {
this.client = new TwitterApi(TWITTER_BEARER_TOKEN);
}
this.stream = this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys'],
'autoConnect': false
}
);
this.stream.on(ETwitterStreamEvent.Data, async (data) => { console.log(data); });
this.stream.on(ETwitterStreamEvent.Error, async (data) => {
console.log('Event:Error', data);
});
this.stream.on(ETwitterStreamEvent.Connected, async () => { console.log('Event:Connected'); });
this.stream.on(ETwitterStreamEvent.ConnectionLost, async () => { console.log('Event:ConnectionLost'); });
this.stream.on(ETwitterStreamEvent.ConnectionError, async (data) => {
console.log('Event:ConnectionError', data);
});
this.stream.on(ETwitterStreamEvent.TweetParseError, async (data) => {
console.log('Event:TweetParseError', data);
});
this.stream.on(ETwitterStreamEvent.ConnectionClosed, async () => { console.log('Event:ConnectionClosed'); });
this.stream.on(ETwitterStreamEvent.ReconnectAttempt, async (data) => {
console.log('Event:ReconnectAttempt', data);
});
this.stream.on(ETwitterStreamEvent.ReconnectError, async (data) => {
console.log('Event:ReconnectError', data);
});
this.stream.on(ETwitterStreamEvent.ReconnectLimitExceeded, async () => { console.log('Event:ReconnectLimitExceeded'); });
this.stream.on(ETwitterStreamEvent.Reconnected, async () => { console.log('Event:Reconnected'); });
this.stream.on(ETwitterStreamEvent.DataKeepAlive, async () => { console.log('Event:DataKeepAlive'); });
try {
await this.stream.connect({
autoReconnect: true,
autoReconnectRetries: Infinity,
});
} catch (error) {
console.log('Unable to establish the first connection. Auto-reconnect will be fired soon.');
console.log(error);
}
} catch (error) {
console.log(error);
}
}
}
const twitter = new Twitter();
(async () => {
await twitter.startStream();
})(); Is this intended behavior, a mistake on my part or possibly a bug? If it's a bug, you can force the I have kinda managed to overcome this problem by implementing my own class Twitter {
async forceReconnect() {
this.stream.destroy();
await new Promise(resolve => setTimeout(() => resolve(), 30000)); // wait 30 seconds
await this.startStream();
}
async startStream() {
// ...
try {
await this.stream.connect({
autoReconnect: true,
autoReconnectRetries: Infinity,
});
} catch (error) {
console.log('Unable to establish the first connection. Auto-reconnect will be fired soon.');
console.log(error);
await this.forceReconnect();
}
} By the way, the new Cheers! |
Hello again!
It also means that class Twitter {
async startStream() {
// await only the first connection to be failed or succeeded
// you must log .on(.ConnectError) to know if the connection fails
await this.stream.connect({
autoReconnect: true,
autoReconnectRetries: Infinity,
});
} |
This is great, I just updated to the latest version, and to no surprise it does exactly as you described 👍 I will update my production server and start using this directly. If I am not overstaying my welcome, I was hoping I could ask you about a connection error that I'm having with Twitter, but one which is probably not going to be directly related to But in any case, thanks for all the updates and the effort you put into this! |
Hey again. So something really strange happened over the course of the past 24 or so hours on the production server. Basically I am experiencing those I have started diligently logging every event and also noticed that at one point the At the point I'm getting totally lost and kinda disheartened in what to do. But in any case, in case you decide to test this on your end, this is my production code, the relevant part of it at least. const { ETwitterStreamEvent, TwitterApi } = require('twitter-api-v2');
class Twitter extends Commands {
async initSources() {
const sourceList = await TwitterSources.find({}).lean();
const streamRules = await this.client.v2.streamRules();
if (streamRules?.data?.length > 0) {
await this.client.v2.updateStreamRules({
delete: {
ids: streamRules.data.map(rule => rule.id),
},
});
}
await this.client.v2.updateStreamRules({
add: sourceList.map(source => ({
value: `(from:${source.name}) -is:retweet -is:reply`,
tag: source.name
}))
});
}
async logToDb({ type, value, path, description }) {
await Errors.create({
...value && { value: JSON.stringify(value, null, 4) },
type: type,
path: path,
...description && { description: description },
clientState: this.client
});
// value: this.client.v2.getLastRateLimitStatus('https://api.twitter.com/2/tweets/search/stream')
}
async startStream() {
try {
if (!this.client) {
this.client = TwitterClient;
}
await this.initSources();
this.stream = this.client.v2.searchStream(
{
"tweet.fields": ['id', 'text', 'created_at', 'entities'],
"user.fields": ['username', 'name', 'profile_image_url'],
'media.fields': ['preview_image_url', 'url', 'width', 'height', 'type'],
'expansions': ['author_id', 'attachments.media_keys'],
'autoConnect': false
}
);
this.stream.on(ETwitterStreamEvent.Data, async (data) => {
await this.handleTweet(data);
});
this.stream.on(ETwitterStreamEvent.DataError, async (data) => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ DataError' });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.DataError',
});
});
this.stream.on(ETwitterStreamEvent.Error, async (data) => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ Error' });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.Error',
});
if (data?.error?.errno === -104 || data?.error?.code === 'ECONNRESET') {
await this.logToChannel({ msg: '**Twitter**:Event:❌❌ Error (ECONNRESET)' });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.Error',
description: 'ECONNRESET'
});
}
});
this.stream.on(ETwitterStreamEvent.Connected, async () => {
await this.logToChannel({ msg: '**Twitter**:Event:✅ Connected' });
await this.logToDb({
type: 'log',
path: 'ETwitterStreamEvent.Connected',
});
});
this.stream.on(ETwitterStreamEvent.ConnectError, async (data) => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ ConnectError' });
await this.logToDb({
type: 'error',
...data && { value: data },
path: 'ETwitterStreamEvent.ConnectError',
});
});
this.stream.on(ETwitterStreamEvent.ConnectionLost, async () => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ ConnectionLost' });
await this.logToDb({
type: 'log',
path: 'ETwitterStreamEvent.ConnectionLost',
});
});
this.stream.on(ETwitterStreamEvent.ConnectionError, async (data) => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ ConnectionError' });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.ConnectionError',
});
});
this.stream.on(ETwitterStreamEvent.TweetParseError, async (data) => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ TweetParseError' });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.TweetParseError',
});
});
this.stream.on(ETwitterStreamEvent.ConnectionClosed, async () => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ ConnectionClosed' });
await this.logToDb({
type: 'error',
path: 'ETwitterStreamEvent.ConnectionClosed',
});
});
this.stream.on(ETwitterStreamEvent.ReconnectAttempt, async (data) => {
await this.logToChannel({ msg: `**Twitter**:Event:❕ ReconnectAttempt (${data})` });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.ReconnectAttempt',
description: 'retries'
});
});
this.stream.on(ETwitterStreamEvent.ReconnectError, async (data) => {
await this.logToChannel({ msg: `**Twitter**:Event:❌ ReconnectError (${data})` });
await this.logToDb({
type: 'error',
value: data,
path: 'ETwitterStreamEvent.ReconnectError',
description: 'retries'
});
});
this.stream.on(ETwitterStreamEvent.ReconnectLimitExceeded, async () => {
await this.logToChannel({ msg: '**Twitter**:Event:❌ ReconnectLimitExceeded' });
await this.logToDb({
type: 'error',
path: 'ETwitterStreamEvent.ReconnectLimitExceeded',
});
});
this.stream.on(ETwitterStreamEvent.Reconnected, async () => {
await this.logToChannel({ msg: '**Twitter**:✅ Event:Reconnected' });
await this.logToDb({
type: 'log',
path: 'ETwitterStreamEvent.Reconnected',
});
});
// this.stream.on(ETwitterStreamEvent.DataKeepAlive, async () => {
// await this.logToChannel({ msg: '**Twitter**:Event:DataKeepAlive' });
// });
try {
await this.stream.connect({
autoReconnect: true,
autoReconnectRetries: Infinity,
nextRetryTimeout: () => 30000,
});
} catch (error) {
console.log(error);
await this.logToChannel({ msg: `**Twitter**:Event:❌ Twitter.stream.connect() Error` });
await this.logToDb({
type: 'error',
value: error,
path: 'Twitter.stream.connect()',
description: 'inner block error'
});
// await this.recoverStream();
}
} catch (error) {
console.log(error);
await this.logToChannel({ msg: `**Twitter**:Event:❌ Twitter.startStream() Error` });
await this.logToDb({
type: 'error',
value: error,
path: 'Twitter.startStream()',
description: 'outer block error'
});
}
}
}
const twitter = new Twitter();
module.exports = twitter; Overall I am not really sure what to think of this. Using version Example error: {
"type": "reconnect error",
"error": {
"error": true,
"type": "response",
"code": 429,
"headers": {
// some private header data removed here
"date": "Fri, 22 Oct 2021 12:46:37 UTC",
"server": "tsa_b",
"content-type": "application/json; charset=utf-8",
"cache-control": "no-cache, no-store, max-age=0",
"content-length": "213",
"x-access-level": "read",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "0",
"x-rate-limit-limit": "50",
"x-rate-limit-reset": "1634907396",
"content-disposition": "attachment; filename=json.json",
"x-content-type-options": "nosniff",
"x-rate-limit-remaining": "39",
"strict-transport-security": "max-age=631138519",
"connection": "close"
},
"rateLimit": {
"limit": 50,
"remaining": 39,
"reset": 1634907396
},
"data": {
"title": "ConnectionException",
"detail": "This stream is currently at the maximum allowed connection limit.",
"connection_issue": "TooManyConnections",
"type": "https://api.twitter.com/2/problems/streaming-connection"
}
},
"message": "Reconnect error - 939 attempts made yet."
} But I actually started logging the state of I saw in your source code that you are using the Not sure what else to add, but in case you're interested in this or it would be of any help to you I can for example give you access to the database where the logs are contained. Not for the purpose of asking you to fix my problem, only in case this is a bug with the library and it will be helpful for you to see this log data, since this error seems to be random and hard to reproduce. In the meantime I will probably try to connect to the Twitter API without a library and see what error I can get and debug that way, if I manage to find my way around. Cheers! |
Hi again, I'm so sorry for all the encountered problems. I really don't think that your code is causing the issue. I'm currently trying to "lock" the instance from making two reconnection requests concurrently somehow.
This process is handled by Then, it closes the request, first by unbinding all events on I'll include a new private property on If you have suggestions, I'll be happy to get some help on this :) |
No issue whatsoever, you are contributing your free time to develop this, I should be thankful more than anything. It's a free product with no warranty.
There is a free tier on Heroku which you can run 24/7 and the setup is quite trivial. They only require a valid credit card, but no payments necessary. This is in fact the same server I am using for this community bot. As for the rest of your comment, I really don't feel competent enough to comment about it because I have limited experience with Node's In any case, this same error occurred again just around 30 min ago and I was around and being able to observe it. Not exactly sure if this is of any help to you, but here are all the events as logged out in my database by the The events are sorted first to last, or in other words the first event in the list below is the first error that occurred. [
{
type: "error",
value: '{ "code": "ECONNRESET" }',
path: "ETwitterStreamEvent.ConnectionError",
date: "2021-10-22T23:16:32.416+03:00",
},
{
type: "error",
value: '{ "type": "connection error", "error": { "code": "ECONNRESET" }, "message": "Connection lost or closed by Twitter." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:32.504+03:00",
},
{
description: "retries",
type: "error",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:16:32.622+03:00"
},
{
type: "error",
value: "{}",
path: "ETwitterStreamEvent.ConnectionError",
date: "2021-10-22T23:16:32.755+03:00"
},
{
type: "error",
value: '{ "type": "connection error", "error": {}, "message": "Connection lost or closed by Twitter." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:32.885+03:00",
},
{
description: "retries",
type: "error",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:16:37.906+03:00"
},
{
type: "log",
path: "ETwitterStreamEvent.Reconnected",
date: "2021-10-22T23:16:38.086+03:00"
},
{
type: "log",
path: "ETwitterStreamEvent.Reconnected",
date: "2021-10-22T23:16:38.198+03:00"
},
{
description: "ECONNRESET",
type: "error",
value: '{ "type": "connection error", "error": { "code": "ECONNRESET" }, "message": "Connection lost or closed by Twitter." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:38.374+03:00",
},
{
type: "error",
value: '{ "errors": [ { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } ] }',
path: "ETwitterStreamEvent.DataError",
date: "2021-10-22T23:16:52.635+03:00",
},
{
type: "error",
value: '{ "type": "data twitter error", "error": { "errors": [ { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } ] }, "message": "Twitter sent a payload that is detected as an error payload." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:52.772+03:00",
},
{
type: "error",
value: "{}",
path: "ETwitterStreamEvent.ConnectionError",
date: "2021-10-22T23:16:52.927+03:00"
},
{
type: "error",
value: '{ "type": "connection error", "error": {}, "message": "Connection lost or closed by Twitter." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:53.038+03:00",
},
{
description: "retries",
type: "error",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:16:53.571+03:00"
},
{
description: "retries",
type: "error",
path: "ETwitterStreamEvent.ReconnectError",
date: "2021-10-22T23:16:58.594+03:00"
},
{
type: "error",
value: '{ "type": "reconnect error", "error": { "error": true, "type": "response", "code": 429, "headers": { "date": "Fri, 22 Oct 2021 20:16:52 UTC", "server": "tsa_b", "content-type": "application/json; charset=utf-8", "cache-control": "no-cache, no-store, max-age=0", "content-length": "213", "x-access-level": "read", "x-frame-options": "SAMEORIGIN", "x-xss-protection": "0", "x-rate-limit-limit": "50", "x-rate-limit-reset": "1634934692", "content-disposition": "attachment; filename=json.json", "x-content-type-options": "nosniff", "x-rate-limit-remaining": "47", "strict-transport-security": "max-age=631138519", "x-response-time": "87", "connection": "close" }, "rateLimit": { "limit": 50, "remaining": 47, "reset": 1634934692 }, "data": { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } }, "message": "Reconnect error - 1 attempts made yet." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:16:58.796+03:00",
},
{
description: "retries",
type: "error",
value: "1",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:17:22.739+03:00",
},
{
description: "retries",
type: "error",
value: "1",
path: "ETwitterStreamEvent.ReconnectError",
date: "2021-10-22T23:17:22.847+03:00",
},
{
type: "error",
value: '{ "type": "reconnect error", "error": { "error": true, "type": "response", "code": 429, "headers": { "date": "Fri, 22 Oct 2021 20:17:22 UTC", "server": "tsa_b", "content-type": "application/json; charset=utf-8", "cache-control": "no-cache, no-store, max-age=0", "content-length": "213", "x-access-level": "read", "x-frame-options": "SAMEORIGIN", "x-xss-protection": "0", "x-rate-limit-limit": "50", "x-rate-limit-reset": "1634934692", "content-disposition": "attachment; filename=json.json", "x-content-type-options": "nosniff", "x-rate-limit-remaining": "46", "strict-transport-security": "max-age=631138519", "x-response-time": "86", "connection": "close" }, "rateLimit": { "limit": 50, "remaining": 46, "reset": 1634934692 }, "data": { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } }, "message": "Reconnect error - 2 attempts made yet." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:17:22.983+03:00",
},
{
description: "retries",
type: "error",
value: "2",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:17:52.891+03:00",
},
{
description: "retries",
type: "error",
value: "2",
path: "ETwitterStreamEvent.ReconnectError",
date: "2021-10-22T23:17:53.003+03:00",
},
{
type: "error",
value: '{ "type": "reconnect error", "error": { "error": true, "type": "response", "code": 429, "headers": { "date": "Fri, 22 Oct 2021 20:17:52 UTC", "server": "tsa_b", "content-type": "application/json; charset=utf-8", "cache-control": "no-cache, no-store, max-age=0", "content-length": "213", "x-access-level": "read", "x-frame-options": "SAMEORIGIN", "x-xss-protection": "0", "x-rate-limit-limit": "50", "x-rate-limit-reset": "1634934692", "content-disposition": "attachment; filename=json.json", "x-content-type-options": "nosniff", "x-rate-limit-remaining": "45", "strict-transport-security": "max-age=631138519", "x-response-time": "85", "connection": "close" }, "rateLimit": { "limit": 50, "remaining": 45, "reset": 1634934692 }, "data": { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } }, "message": "Reconnect error - 3 attempts made yet." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:17:53.101+03:00",
},
{
description: "retries",
type: "error",
value: "3",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:18:23.106+03:00",
},
{
description: "retries",
type: "error",
value: "3",
path: "ETwitterStreamEvent.ReconnectError",
date: "2021-10-22T23:18:23.218+03:00",
},
{
type: "error",
value: '{ "type": "reconnect error", "error": { "error": true, "type": "response", "code": 429, "headers": { "cache-control": "no-cache, no-store, max-age=0", "connection": "close", "content-disposition": "attachment; filename=json.json", "content-length": "213", "content-type": "application/json; charset=utf-8", "date": "Fri, 22 Oct 2021 20:18:23 GMT", "server": "tsa_b", "strict-transport-security": "max-age=631138519", "x-access-level": "read", "x-content-type-options": "nosniff", "x-frame-options": "SAMEORIGIN", "x-rate-limit-limit": "50", "x-rate-limit-remaining": "44", "x-rate-limit-reset": "1634934692", "x-response-time": "100", "x-xss-protection": "0" }, "rateLimit": { "limit": 50, "remaining": 44, "reset": 1634934692 }, "data": { "title": "ConnectionException", "detail": "This stream is currently at the maximum allowed connection limit.", "connection_issue": "TooManyConnections", "type": "https://api.twitter.com/2/problems/streaming-connection" } }, "message": "Reconnect error - 4 attempts made yet." }',
path: "ETwitterStreamEvent.Error",
date: "2021-10-22T23:18:23.366+03:00",
},
{
description: "retries",
type: "error",
value: "4",
path: "ETwitterStreamEvent.ReconnectAttempt",
date: "2021-10-22T23:18:53.307+03:00",
},
{
description: "retries",
type: "error",
value: "4",
path: "ETwitterStreamEvent.ReconnectError",
date: "2021-10-22T23:18:53.486+03:00",
},
]; From here on |
Hello again, |
Hey @alkihis I just wanted to let you know that ever since I updated to As you will notice, there is a single disconnect since then, but the stream managed to recover quickly. It was an The only unusual thing I've noticed here is that there was a seemingly random But anyway, I am not really sure what to make out of all of this and what exactly fixed the problem, but if it works then it works and that makes me happy. I thought all of this was worth mentioning, since in one of my comments here I suggested you could try reproducing the same error on Heroku, but at the moment, if you use the same code and same Heroku server I am not really sure that is going to yield any useful results (in the form of errors and disconnects). Nevertheless, I have no intentions of stopping the bot, so it will keep on running it and if I run into the same problem, or a different problem then I shall let you know. |
Hey @alkihis, I was actually about to reopen my previous issue again, but skimmed this thread and related my issue. I haven't looked into the package source yet, but how is it the reconnect function works to actually restore the connection? Does it try and retrieve the previous connection that it had to Twitter or does it create & reauthenticate a brand new connection based off of the exact same tweet stream parameters in the object? My thoughts are for this because I've observed in my production env logs, recovering from a connection issue when it was an operational disconnect which is just a short blip from twitter disconnecting, that just requires requires recovering that connection (probably from Twitter doing work on their cluster requiring the connection to close. That's what I understand operational disconnect as anyway) Now I wonder if this is what's actually happening because Twitter destroys the previous connection on their end after 20 seconds from losing it, we're trying to connect to that previous stream and are unable to because twitter has destroyed it? Where after 20 seconds we should be authenticating again & creating a brand new connection? I am a complete noob regarding how the requests actually work and have no knowledge on how the reconnect functions currently, so everything I've just said could be logical nonsense. Before I version bumped to the release adding missing endpoints, I listened to the disconnect/connection close Emitter and had it fire I hope what I've typed with limited knowledge makes sense and also congrats on becoming the main maintainer! :) |
For Example after looking at the TweetStream src, we call the node-twitter-api-v2/src/stream/TweetStream.ts Lines 303 to 333 in 27fd130
Would it make more sense to node-twitter-api-v2/src/stream/TweetStream.ts Lines 208 to 228 in 27fd130
rather than try and node-twitter-api-v2/src/stream/TweetStream.ts Lines 274 to 301 in 27fd130
Again I'm really not sure how a lot of this works and could be getting it completely wrong but suggestions are suggestion there is the chance that I may be right 😅 |
At this point I am almost certain this is a problem with the Twitter API itself, there are a few threads on twittercommunity.com such as this one or this one dating back from October last year. It doesn't seem like Twitter devs care enough to fix it. Some user is suggesting to "Simply wrap your requests calls in a “with” context block and it closes unused connections that come about when your script stops unexpectedly." but I honestly don't even know what that's supposed to mean. Also I got another async sockets() {
try {
const { stdout, stderr } = await exec('ss -t | grep 104.244');
if (stdout) {
return stdout.replace(/\s+/g, ' ');
}
else if (stderr) {
return stderr;
}
} catch (error) {
if (error?.code === 1) {
return 'No open sockets at 104.244.x.x'
}
else {
return error;
}
}
} What this showed me is that there were in fact zero established or ongoing connections while {
"type": "reconnect error",
"error": {
"error": true,
"type": "response",
"code": 429,
"headers": {
"date": "Wed, 27 Oct 2021 18:40:00 UTC",
"server": "tsa_b",
"content-type": "application/json; charset=utf-8",
"cache-control": "no-cache, no-store, max-age=0",
"content-length": "213",
"x-access-level": "read",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "0",
"x-rate-limit-limit": "50",
"x-rate-limit-reset": "1635360749",
"content-disposition": "attachment; filename=json.json",
"x-content-type-options": "nosniff",
"x-rate-limit-remaining": "44",
"strict-transport-security": "max-age=631138519",
"x-response-time": "88",
"connection": "close"
},
"rateLimit": {
"limit": 50,
"remaining": 44,
"reset": 1635360749
},
"data": {
"title": "ConnectionException",
"detail": "This stream is currently at the maximum allowed connection limit.",
"connection_issue": "TooManyConnections",
"type": "https://api.twitter.com/2/problems/streaming-connection"
}
},
"message": "Reconnect error - 6 attempts made yet."
} Obviously this can not be true, because there were no active/established/ongoing TCP connections to
EDIT: Ignore the last 4 paragraphs, the error happened again and neither restarting the Heroku server, nor launching the bot on my local dev server managed to successfully connect, same error |
See I'm not entirely sure that it is an error with their Twitter API I think their connection limit and rate limits are functioning correctly as per their documentation, I think it may be a case of the reconnection strategy of the package may need revised after reviewing the API reference and documentation. It could be we are trying to reconnect too quickly which is causing problems. I'm not sure though, I'll include the API Reference related to this issue:
|
@Zedifuu you might be on to something, but I'm not sure either. The full quote here is:
The thing is though, at least in my case I am not getting
Are you using the custom https://github.com/PLhery/node-twitter-api-v2/blob/master/doc/streaming.md#methods--properties It basically lets you define your own login on how long to wait between reconnect attempts. Mine is currently set to 30 seconds flat, but it might be worth experimenting with 1 minute or 2 minutes for example, just to see if it will make any different. It's going to be really annoying to reproduce though, you basically have to deploy to production like that and then wait couple of day until the connection drops... |
So there isn't really a response code for Oooooooo I have not used and then there is of course looking at other established packages as well and understanding how they document & tackle the same problem. Cause we're all using the same endpoints here, so it's the case that this its' already been probably discussed. For example that rate limit segment you just posted there, have a look at the troubleshooting section of Twitter-lite: Did that rate limit by any chance expire 26/27 hrs ago? :> |
I have found when working in my dev env, if I Least were not getting told to Enhance Our Calm :P So if you're receiving From the looks of things node-twitter-api-v2/src/stream/TweetStream.ts Lines 306 to 322 in d73426e
node-twitter-api-v2/src/stream/TweetStream.ts Lines 287 to 290 in d73426e
Maybe add 20/30 sec, delay before starting a new connection in the above functions? |
@Zedifuu a lot of info to process here, but assuming that you are correct about everything, what I was thinking modifying the await this.stream.connect({
autoReconnect: true,
autoReconnectRetries: Infinity,
nextRetryTimeout: (tryOccurence, error) => {
if (error?.code === 429) {
/*
So the idea here is the following:
1. Pass the error object down the nextRetryTimeout so we have access to it.
2. Check if the error code is 429.
*/
if (tryOccurence === 0) {
/*
If this is the first time you're getting 429-ed,
don't even attempt to reconnect immediately,
and instead wait 1 minute before making another attempt.
*/
return 60000; // 60000 milliseconds = 1 minute
}
else {
/*
If the first reconnect attempt after 60 seconds wasn't succesful,
futher increase the wait time between upcoming attempts, up to 5 minutes.
*/
return tryOccurence * 60000 < 300000 ? tryOccurence * 60000 : 300000; // or some other logic here
}
}
else {
/*
If this isn't a 429 error, then handle things the usual way.
*/
return 30000;
}
},
}); In order for this to work, the
Well, if this is indeed true, I am not sure if the initial check of |
Ooooo nice, only thing though, node-twitter-api-v2/src/stream/TweetStream.ts Line 253 in d73426e
I'm looking at as we're getting the 429 in the first place, a sufficient reconnect strategy we should not be getting a 429, so if the methods were using to space out the reconnect attempts is sound, are the functions we're using to reconnect at fault? |
Very good point, and very good question, somehow I did not think of that. I suppose we can wait for the developer's (@alkihis) input on this, and perhaps in the meantime until they reply, try something on our own. I am thinking, maybe just to prove the point, add an artificial timeout of 1 or 2 minutes, right before line 253, in order to delay the initial |
My thoughts exactly :) We certainly have given a lot of ideas and reading material anyway 😄 Thats an idea! how about on line 289? This would however delay all connections including initial which isn't great node-twitter-api-v2/src/stream/TweetStream.ts Lines 285 to 290 in d73426e
in the reconnect function ? Since everything ( onConnectionError , .connect ) seems to be calling reconnect ?
|
Gonna keep it ultra short this time. Another 15 minutes since the initial |
Okay, a lot of discussion happend here in a week.
The changes discussed before (up to 15 minutes for Hope it will help you to resolve your issues. |
Thank you for dedicating time to test this @alkihis, and sad to see that unfortunately you were unable to reproduce the exact same error/behavior. I suppose it's kinda random, but which exact clients or environments are affected here is hard to tell.
Sorry, I am not sure I completely understand this part, are you saying that the If it's the former and you are certain about this, then I guess there is not much that can be done here. @Zedifuu speculated that an immediate reconnect attempt is what makes matters worse, but if you're right about this then that theory would be invalid. As for the upcoming changes with a Having the The |
Hey @alkihis, I just wanted to let you know that I haven't had any more 15 minute reconnect loops since the 15th of December last year, so that's around 20 days ago. I really haven't done or changed anything since my last comment in November, because I ran out of time and patience to deal with this issue and just decided to live with it. But in any case, this just confirms what I suspected, which is that it was a problem with Twitter (or less likely with Heroku) that they have eventually fixed, and not a problem with this library. So that's all, and finally I also wanted to say thank you again for providing help and even going lengths trying to reproduce the problem on your end, it is appreciated. I'm closing this issue now because it's resolved (and hopefully it stays that way). Cheers! |
Hey guys, I forgot to also check back in, I have also stopped having reconnect rate limits also. I have a funny feeling this was fixed quietly by twitter when they released API v2 from Early Access I feel I was at most part trying to diagnose an issue that wasn't caused by us 😅 Wish you guys all the best with what you create And again @alkihis excellent work again, Thank you both for the chat! |
Hey is there anyone who is still getting this issue, i am facing this issue with stream. Now stream just stop listening to events and doesn't give any error. i have to manually restart the stream after 2 to 3 days. |
This is not the same issue as the one that we were discussing here for the most part. At least in my case I was unable to recover the connection after getting an What I could quickly suggest is to bind a listener for the following events: https://github.com/PLhery/node-twitter-api-v2/blob/master/doc/streaming.md#events I would also suggest creating a separate issue, the maintainers and devs of this library are quite helpful so they would probably help if you provide them with additional details. By the way, despite this comment shortly after I started getting ECONNRESETs again, but I just don't have time, desire or energy to go down this rabbit hole again, so whatever. A few disconnects per month (5-10) isn't that big of a deal. It's most likely an issue with Twitter or less likely with Heroku. |
Hello folks, thanks for developing and maintaining this library. I have a question, or two questions actually if you don't mind.
Right now I am running a bot that is connected to a Twitter stream, which listens for tweets from an array of users, and then I do some fancy stuff with it. The problem is, every once in a while, Twitter throws a 429 error at me, maybe once every 24-48 hrs, and as a result of that I get an unhandledRejection error, and then the Stream no longer works without manually restarting the bot.
Here is a simplified version of my code:
So as you can see, I am creating a
_Twitter
class, and then creating an instance of that class to connect to the Streaming API. This all works fine, except that only theData
andDataKeepAlive
events are emitting properly, and the rest goes into mycatch
block.It also appears thatautoReconnect
andautoReconnectRetries
are not a properties ofthis.stream
, andreconnect()
is not a valid method either. I think the problem is that I have to use and implement theTweetStream
into my code, but I am not sure how to exactly, and I can't find documentation for this either.In any case, I don't want to make this too long, so I would just like to ask if you can point me in the right direction, such as providing a very quick example if that is possible.
Thank you for your time.
EDIT: It appears that
this.stream
is in fact aTweetStream
object, there was just an error with my test script.I would still like to ask, is this the proper way to implement
autoReconnect
? The actual code on my server does not have thistry catch
block, because I just assumed it would reconnect automatically on any kind of connection error. I'm not sure how to test this properly because Twitter only allows for 1 concurrent stream.The text was updated successfully, but these errors were encountered: