بازگشت به وبلاگ
خطای 409 در ربات تلگرام؛ چرا `getUpdates` با خطای Conflict مواجه می‌شود؟

خطای 409 در ربات تلگرام؛ چرا `getUpdates` با خطای Conflict مواجه می‌شود؟

۱۴۰۵/۶/۱۳admin

وقتی ربات تلگرام شما با خطای 409 متوقف می‌شود؛ مشکل از کجاست؟

همه‌چیز خوب پیش می‌رود.

ربات تلگرام را نوشته‌اید، توکن را وارد کرده‌اید، چند Handler ساخته‌اید و در نهایت برنامه را اجرا می‌کنید:

bot.infinity_polling()

ربات شروع به کار می‌کند و پیام‌ها را دریافت می‌کند.

اما ناگهان در ترمینال با این خطا مواجه می‌شوید:

Error code: 409

Conflict: terminated by other getUpdates request;
make sure that only one bot instance is running

اگر اولین بار است که این خطا را می‌بینید، احتمالاً سؤال اولتان این است:

«کجای کدم را اشتباه نوشته‌ام؟»

خبر خوب این است که در بیشتر مواقع مشکل از کد شما نیست.

تلگرام در واقع دارد یک پیام نسبتاً ساده به شما می‌دهد:

یک برنامهٔ دیگر هم دارد پیام‌های همین ربات را دریافت می‌کند.

در این مقاله می‌خواهیم ببینیم این خطا دقیقاً چرا اتفاق می‌افتد، getUpdates چه کاری انجام می‌دهد و چطور می‌توانیم مشکل را پیدا و برطرف کنیم.


اول ببینیم ربات تلگرام چطور پیام دریافت می‌کند

قبل از اینکه سراغ خطای 409 برویم، بهتر است یک قدم به عقب برگردیم.

وقتی کاربری برای ربات شما پیامی می‌فرستد، پیام ابتدا به سرورهای تلگرام می‌رسد.

مثلاً کاربر می‌فرستد:

سلام

اما ربات شما چگونه متوجه می‌شود که چنین پیامی دریافت شده است؟

یکی از روش‌های دریافت این پیام، استفاده از getUpdates است.

در این روش، برنامهٔ شما مرتباً از تلگرام می‌پرسد:

آیا پیام جدیدی برای من داری؟

اگر پیام جدیدی وجود داشته باشد، تلگرام آن را به برنامهٔ شما برمی‌گرداند.

به این روش معمولاً Long Polling گفته می‌شود.


infinity_polling() در واقع چه کار می‌کند؟

وقتی در TeleBot می‌نویسیم:

bot.infinity_polling()

کتابخانه شروع می‌کند به دریافت مداوم Updateها از تلگرام.

به صورت ساده می‌توانیم ماجرا را این‌طور تصور کنیم:

ربات
  │
  │  «پیام جدیدی داری؟»
  ▼
تلگرام
  │
  │  «بله، این هم پیام»
  ▼
ربات
  │
  │  «پیام دیگری داری؟»
  ▼
تلگرام
  │
  │  ...
  ▼

این چرخه تا زمانی که برنامهٔ شما در حال اجراست ادامه پیدا می‌کند.

پس وقتی infinity_polling() را اجرا می‌کنید، برنامهٔ شما عملاً یک ارتباط دائمی برای دریافت پیام‌های ربات برقرار می‌کند.

و همین‌جا نکتهٔ مهم ماجرا شروع می‌شود.


تلگرام نمی‌خواهد دو برنامه هم‌زمان این کار را انجام دهند

فرض کنید ربات خود را روی کامپیوترتان اجرا کرده‌اید:

کامپیوتر
   │
   └── bot.py
          │
          └── getUpdates
                 │
                 ▼
              تلگرام

همه‌چیز خوب است.

حالا بدون اینکه برنامهٔ اول را متوقف کنید، دوباره آن را اجرا می‌کنید:

python bot.py

اکنون دو برنامه دارید:

کامپیوتر
   │
   ├── bot.py #1
   │       │
   │       └── getUpdates
   │
   └── bot.py #2
           │
           └── getUpdates

هر دو برنامه از یک Bot Token استفاده می‌کنند.

یعنی از دید تلگرام:

یک ربات
   │
   ├── برنامهٔ اول
   │
   └── برنامهٔ دوم

هر دو نیز می‌گویند:

من می‌خواهم Updateهای این ربات را دریافت کنم.

اینجاست که تلگرام وارد عمل می‌شود و یکی از درخواست‌ها را متوقف می‌کند.

نتیجه:

409 Conflict

چرا اسم خطا Conflict است؟

کد 409 در اینجا به معنی تعارض است.

یعنی درخواست شما از نظر ساختار ممکن است کاملاً درست باشد، اما با وضعیت فعلی سرویس سازگار نیست.

در مورد ربات تلگرام، بخش مهم خطا این است:

terminated by other getUpdates request

اگر بخواهیم خیلی ساده ترجمه‌اش کنیم:

«درخواست getUpdates شما به خاطر وجود یک درخواست getUpdates دیگر متوقف شد.»

و خود تلگرام هم در ادامه تقریباً علت را مستقیم بیان کرده است:

make sure that only one bot instance is running

یعنی:

«مطمئن شوید که فقط یک نمونه از ربات در حال اجراست.»

بنابراین خود پیام خطا تقریباً راه‌حل را هم به ما گفته است.


یک سناریوی واقعی

فرض کنید در حال توسعهٔ یک ربات هستید.

ترمینال را باز می‌کنید:

python bot.py

ربات شروع به کار می‌کند.

حالا کد را تغییر می‌دهید و دوباره در یک ترمینال دیگر می‌نویسید:

python bot.py

ممکن است فکر کنید:

«خب، این فقط یک بار دیگر برنامه را اجرا کردم.»

اما سیستم‌عامل این‌طور به قضیه نگاه نمی‌کند.

در واقع دو Process مستقل دارید:

Process 1
    python bot.py
        ↓
    getUpdates


Process 2
    python bot.py
        ↓
    getUpdates

هر دو Process به تلگرام متصل هستند و هر دو از همان Token استفاده می‌کنند.

در نتیجه یکی از آن‌ها مرتباً با خطای 409 مواجه خواهد شد.


اما گاهی ما اصلاً نمی‌دانیم برنامهٔ دوم کجاست!

این قسمت معمولاً جالب‌ترین بخش ماجراست.

گاهی شما مطمئن هستید که فقط یک بار ربات را اجرا کرده‌اید، اما باز هم خطای 409 دریافت می‌کنید.

در این حالت ممکن است برنامهٔ دوم در جایی باشد که اصلاً یادتان نیست.

مثلاً:

  • یک ترمینال دیگر
  • یک پنجرهٔ دیگر از VS Code
  • یک سرور قدیمی
  • یک VPS
  • یک Docker Container
  • یک سرویس systemd
  • یک برنامهٔ اجراشده در پس‌زمینه
  • یا حتی کامپیوتر دیگری که قبلاً ربات را روی آن اجرا کرده‌اید

مثلاً:

لپ‌تاپ شما
    │
    └── bot.py
          │
          └── getUpdates


VPS
    │
    └── bot.py
          │
          └── getUpdates

از نظر شما شاید فقط یک ربات وجود داشته باشد.

اما از نظر تلگرام دو برنامه در حال کار هستند.


یکی از رایج‌ترین اشتباه‌ها: ربات روی کامپیوتر و سرور هم‌زمان اجرا می‌شود

این اتفاق مخصوصاً هنگام انتقال پروژه از محیط توسعه به سرور بسیار رایج است.

فرض کنید ربات را روی لپ‌تاپ توسعه داده‌اید و همه‌چیز درست کار می‌کند.

بعد پروژه را روی VPS منتقل می‌کنید و آنجا نیز ربات را اجرا می‌کنید.

اما ربات روی لپ‌تاپ همچنان باز است.

در نتیجه:

                 Telegram
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
       Laptop                VPS
          │                   │
     getUpdates          getUpdates

هر دو با یک Token.

نتیجه همان 409 Conflict است.

پس اگر ربات را از محیط توسعه به سرور منتقل کرده‌اید، اولین چیزی که باید بررسی کنید این است که نسخهٔ قبلی هنوز روی سیستم شخصی‌تان اجرا نباشد.


چطور بفهمیم چند نسخه از ربات در حال اجراست؟

اگر روی Linux یا VPS هستید، می‌توانید Processهای مربوط به ربات را بررسی کنید.

مثلاً:

ps aux | grep bot.py

یا:

ps aux | grep python

اگر چیزی شبیه این دیدید:

user   1234  ... python bot.py
user   5678  ... python bot.py

احتمالاً دو نسخه از ربات در حال اجراست.

می‌توانید Process اضافی را متوقف کنید:

kill 5678

البته باید مطمئن باشید که شناسهٔ موردنظر واقعاً مربوط به ربات شماست.


اگر از Docker استفاده می‌کنید

در پروژه‌هایی که با Docker اجرا می‌شوند، باید Containerها را هم بررسی کنید:

docker ps

فرض کنید خروجی شما نشان می‌دهد دو Container مربوط به ربات فعال هستند:

container_1
container_2

اگر هر دو Container برنامهٔ یکسانی را با یک Bot Token اجرا کنند:

container_1
      │
      └── getUpdates
             │
             ▼
          Telegram
             ▲
             │
      ┌──────┘
      │
container_2
      │
      └── getUpdates

باز هم همان خطا را خواهید داشت.

در معماری مبتنی بر Polling، باید دقت کنید که چند نمونه از برنامه به طور هم‌زمان مسئول دریافت Updateها نباشند.


systemd هم می‌تواند مقصر باشد!

فرض کنید روی سرور ربات را به صورت یک سرویس اجرا کرده‌اید:

systemd
   │
   └── bot.service
          │
          └── python bot.py

سرویس در پس‌زمینه فعال است.

حالا شما وارد سرور می‌شوید و برای تست می‌نویسید:

python bot.py

ممکن است متوجه نباشید که یک نسخهٔ دیگر از ربات همین الان توسط systemd در حال اجراست.

برای بررسی:

systemctl status mybot

اگر سرویس فعال باشد، باید مراقب باشید که همان ربات را دوباره به صورت دستی اجرا نکنید.


آیا این خطا به Threadهای Python مربوط است؟

در Traceback ممکن است این عبارت را ببینید:

Threaded polling exception

و شاید فکر کنید:

«پس مشکل از Thread است!»

اما لزوماً این‌طور نیست.

بخش مهم‌تر Traceback شما این است:

get_updates(...)

و بعد:

Error code: 409

کتابخانهٔ TeleBot می‌تواند برای مدیریت Polling و پردازش Updateها از Thread استفاده کند.

اما خطایی که Telegram API برگردانده، مشخصاً مربوط به تعارض در getUpdates است.

پس بهتر است ابتدا سراغ Threadها نرویم.

اول بررسی کنیم:

آیا یک برنامهٔ دیگر با همین Token در حال Polling است؟

در اکثر مواقع همین سؤال مشکل را حل می‌کند.


آیا مشکل از TeleBot است؟

در اغلب موارد، خیر.

این نکته مهم است، چون وقتی یک Exception از داخل کتابخانه می‌بینیم، اولین واکنش طبیعی این است که فکر کنیم کتابخانه مشکل دارد.

اما این قسمت:

telebot.apihelper.ApiTelegramException

فقط نشان می‌دهد که Telegram API یک خطا برگردانده و کتابخانهٔ TeleBot آن خطا را به صورت Exception به برنامهٔ شما منتقل کرده است.

مسیر اتفاق تقریباً این است:

کد شما
   │
   ▼
TeleBot
   │
   ▼
getUpdates
   │
   ▼
Telegram API
   │
   ▼
409 Conflict
   │
   ▼
TeleBot Exception

بنابراین TeleBot در اینجا بیشتر پیام‌رسان خطا است تا علت آن.


یک نکته دربارهٔ Webhook

تلگرام برای دریافت Update فقط getUpdates ندارد.

روش دیگری نیز وجود دارد که به آن Webhook می‌گوییم.

در Polling، برنامهٔ ما از تلگرام درخواست می‌کند:

«پیام جدید داری؟»

اما در Webhook، تلگرام خودش پیام را به آدرس مشخص‌شدهٔ برنامهٔ ما ارسال می‌کند:

Telegram
    │
    │ HTTP Request
    ▼
Server

این دو روش معماری متفاوتی دارند و باید بدانیم رباتمان دقیقاً از کدام روش استفاده می‌کند.

اگر از infinity_polling() استفاده می‌کنید، به احتمال زیاد دارید از Polling استفاده می‌کنید.

در چنین شرایطی باید مطمئن شوید یک Polling دیگر با همان Token فعال نیست.


آیا delete_webhook() همیشه راه‌حل است؟

ممکن است در اینترنت با راه‌حل‌هایی مثل این مواجه شوید:

bot.delete_webhook()
bot.infinity_polling()

این کد در برخی شرایط مفید است، اما نباید آن را به عنوان درمان قطعی خطای 409 در نظر گرفت.

اگر واقعاً مشکل این باشد که دو برنامه در حال getUpdates هستند، حذف Webhook مشکل اصلی را حل نمی‌کند.

مثلاً اگر وضعیت این باشد:

Laptop
   │
   └── getUpdates


VPS
   │
   └── getUpdates

نوشتن:

bot.delete_webhook()

باعث نمی‌شود Laptop ناپدید شود!

مشکل اصلی همچنان دو برنامهٔ فعال است.


بهترین روش برای پیدا کردن مشکل

اگر با خطای 409 مواجه شدید، بهتر است به جای تغییر تصادفی کد، مرحله‌به‌مرحله جلو بروید.

مرحلهٔ اول: اجرای محلی را بررسی کنید

آیا ربات در یک Terminal دیگر باز است؟

آیا VS Code هنوز برنامه را اجرا می‌کند؟

آیا IDE دیگری آن را اجرا کرده است؟


مرحلهٔ دوم: سرور را بررسی کنید

اگر ربات روی VPS یا Server قرار دارد:

ps aux | grep bot.py

را اجرا کنید.


مرحلهٔ سوم: Docker را بررسی کنید

اگر Docker دارید:

docker ps

را بررسی کنید.


مرحلهٔ چهارم: سرویس‌ها را بررسی کنید

اگر از systemd استفاده می‌کنید:

systemctl status mybot

را بررسی کنید.


مرحلهٔ پنجم: محل‌های دیگر را فراموش نکنید

آیا ربات هنوز روی:

  • لپ‌تاپ
  • کامپیوتر قبلی
  • VPS قدیمی
  • سرور آزمایشی
  • محیط توسعه
  • Docker
  • یا یک سرویس ابری

در حال اجراست؟

این سؤال گاهی مهم‌تر از بررسی خود کد است.


یک ساختار ساده و مناسب برای ربات

برای یک ربات ساده که قرار است با Polling کار کند، می‌توانیم ساختاری مانند این داشته باشیم:

import telebot

TOKEN = "YOUR_BOT_TOKEN"

bot = telebot.TeleBot(TOKEN)


@bot.message_handler(commands=["start"])
def start(message):
    bot.reply_to(
        message,
        "سلام! به ربات خوش آمدید."
    )


if __name__ == "__main__":
    bot.infinity_polling()

نکتهٔ مهم این قسمت است:

if __name__ == "__main__":
    bot.infinity_polling()

این کار باعث می‌شود Polling هنگام اجرای مستقیم فایل شروع شود.

اما باز هم باید به خاطر داشته باشیم:

این کد جلوی اجرای هم‌زمان دو Process را نمی‌گیرد.

اگر فایل را دوبار اجرا کنید، همچنان دو Process خواهید داشت.


یک اشتباه معماری که باید مراقبش باشیم

فرض کنید برای افزایش سرعت برنامه تصمیم می‌گیریم چند Worker داشته باشیم:

Worker 1 ──> getUpdates
Worker 2 ──> getUpdates
Worker 3 ──> getUpdates

این کار برای Polling تلگرام را نمی‌توان مانند افزایش سادهٔ Workerهای یک وب‌سرور در نظر گرفت.

در یک وب‌سرویس معمولی، ممکن است چند Worker کاملاً طبیعی باشد.

اما وقتی چند Process مستقیماً می‌خواهند Updateهای یک Bot را با getUpdates دریافت کنند، موضوع متفاوت است.

پس قبل از اینکه تعداد Workerها را افزایش دهیم، باید معماری دریافت Updateها را در نظر بگیریم.


خلاصهٔ مشکل در یک تصویر

اگر بخواهیم کل این مقاله را در یک نمودار خلاصه کنیم، وضعیت صحیح این است:

             Bot Token
                 │
                 ▼
            یک برنامه
                 │
                 ▼
             getUpdates
                 │
                 ▼
              Telegram

اما وضعیت مشکل‌دار این است:

                  Bot Token
                      │
              ┌───────┴───────┐
              │               │
              ▼               ▼
          برنامهٔ اول      برنامهٔ دوم
              │               │
         getUpdates       getUpdates
              │               │
              └───────┬───────┘
                      ▼
                   Telegram
                      │
                      ▼
                409 Conflict

در نهایت چه کاری باید انجام دهیم؟

وقتی خطای زیر را دیدید:

409 Conflict:
terminated by other getUpdates request

قبل از اینکه سراغ تغییر کد بروید، چند سؤال ساده از خودتان بپرسید:

آیا ربات را دوبار اجرا کرده‌ام؟

آیا ربات روی سرور دیگری فعال است؟

آیا یک Process قدیمی هنوز در حال اجراست؟

آیا Docker Container دیگری دارم؟

آیا systemd ربات را در پس‌زمینه اجرا می‌کند؟

آیا ربات را هم روی کامپیوتر خودم و هم روی VPS اجرا کرده‌ام؟

اگر جواب یکی از این سؤال‌ها «بله» باشد، احتمالاً دلیل خطا را پیدا کرده‌اید.


نتیجه‌گیری

خطای 409 Conflict در ربات‌های تلگرام ممکن است در نگاه اول یک خطای پیچیده به نظر برسد، مخصوصاً وقتی همراه با یک Traceback طولانی از TeleBot نمایش داده می‌شود.

اما اگر پیام خطا را با دقت بخوانیم، تلگرام تقریباً مستقیماً به ما می‌گوید مشکل چیست:

terminated by other getUpdates request

یعنی یک درخواست دیگر برای getUpdates وجود دارد.

بنابراین در ساده‌ترین شکل می‌توان گفت:

برای یک Bot Token، نباید چند برنامهٔ مستقل به طور هم‌زمان با روش getUpdates در حال دریافت Update باشند.

در نتیجه، وقتی با این خطا روبه‌رو شدید، قبل از اینکه کتابخانه را عوض کنید، کد را از نو بنویسید یا Token جدید بسازید، ابتدا محیط اجرای برنامه را بررسی کنید.

بسیاری از خطاهای مربوط به ربات‌ها نه در چند خط کدی که نوشته‌ایم، بلکه در نحوهٔ اجرای همان کد اتفاق می‌افتند.

و خطای 409 Conflict یکی از بهترین مثال‌های همین موضوع است.

نظرات

برای ارسال نظر باید وارد شوید.