The post has been translated automatically. Original language: Russian
In my project, I automated a simple scenario: process a link from a Telegram message, receive a media file and send it to the same chat. I put together the first working version in less than a day. After the launch, work began on less obvious situations: re-delivery of events, errors in external services, and limited execution time.
Below is an analysis of specific errors, their causes and verification methods. These situations can also occur in other applications that accept webhooks, use a cache, and access paid APIs.
1. Architecture and execution boundaries
I chose Python, FastAPI, and yt-dlp to start with. The bot works via a webhook: Telegram sends an event, the function on Vercel processes the link and sends the file to the chat. A constantly running server was not needed for this scenario.
One operation included several steps: to recognize the link, check the cache, receive the file and send the result. All the steps had to be completed in the time allocated to the server function.
But a successful download on my computer did not mean that the same link would work after deployment. YouTube and Instagram could restrict requests from the IP address of the datacenter. Different types of publications also had different behavior: the video was downloaded, but the photo or the carousel was no longer there.
I had to build several ways to get content. The bot starts with a direct request, and in some cases uses additional options, including the paid Apify service. Each such option adds waiting time, the possibility of error, and sometimes costs.
2. Reliability starts with repetitions and unexpected data.
The first noticeable bug looked simple: one video sometimes arrived twice. While the bot was downloading the file for a long time, Telegram re-delivered the event, and processing was started again. The link should have been marked as being in progress immediately, before downloading.
Then I added the cache. After sending the file, Telegram returns its identifier, which can be reused. I saved these IDs in Redis: the familiar link no longer needs to be downloaded and downloaded again every time.
However, the cache was almost not triggered. The "Share" button added service parameters to the links, so one video from two people looked like two different addresses. It helped to bring the links to a single view before searching in the cache.
Another glitch was found on the border with Redis. The code handled the error that the service returned, but skipped the exception when the connection was terminated. Because of this, the link could go unanswered. Checking with a really inaccessible address showed something that the test with the conditional answer "500" did not show.
Since then, when checking integrations, I have been looking separately at what happens if the service responds with an error, does not respond at all, or responds too late.
3. A bug that I fixed three times
Vertical videos on the phone were sometimes displayed square, although everything looked fine on the computer.
First, I started transmitting the width and height of the video to Telegram. Then I decided that the preview was the problem and started generating it myself. Both versions sounded convincing, but they didn't help.
The reason was found when I took a specific problematic link and looked at the real data: the library returned empty width and height values. The code "transmitted dimensions," but there was nothing to transmit. As a result, I began to read them from the file itself.
Similar surprises occurred when new platforms were connected. For example, the Reddit gallery could walk in a circle between two addresses. Each individual request was successful, but the entire operation did not end. In this case, you need a time limit for the entire processing.
These stories have changed the way I work: first, I reproduce the problem and see what really comes and goes, then I choose a fix.
4. When a technical error becomes an expense
With the introduction of paid services, timeouts began to affect money.
In one scenario, the bot started a paid download when there were about 14 seconds left until the end of the available time. The download itself could take up to 50 seconds. The function was terminated, the external service continued to work, but the user did not receive the result. Re-delivering the event could trigger another paid request.
Now, before such a launch, it is checked whether there is enough time left. For long operations in the Mini App, I used a different approach: the request starts processing and returns the status, and the interface later requests the result. An already started task can be reused, even if several people have opened the same profile at the same time.
Another principle has also appeared here: the script, which is free for the user, must still be counted in the project costs. I have introduced separate budgets for accessing paid services so that the activity of one user does not create unlimited costs.
5. Why the query counter does not replace the budget
Accessing external services may have different costs. In my case, the costs depended, among other things, on the duration of the media being processed. Therefore, the same number of requests from two users did not mean the same costs.
For paid YouTube processing, I began to reserve a budget before starting a task, and after receiving the result, recalculate the expense according to the actual duration. The costs of different integrations are accounted for separately so that one category of requests does not exhaust the budget of the other.
There was an accounting error here: immediately after completing the task, the service showed the preliminary cost, and the final charge appeared later. If you immediately return the difference between the reserve and the preliminary amount, the available budget will be overstated.
In this scenario, I switched to calculating the duration of the result and the prices of the selected handler. For similar integration, it is useful to find out separately when the provider finally fixes the cost, and check the behavior when this data is delayed.
Another solution concerned an inaccessible database: if the budget cannot be checked, the paid operation does not start. Otherwise, accounting failure turns into an opportunity to spend money uncontrollably.
6. How to process a repeated payment event
Enabling payments via Telegram Stars has added another case of repeat delivery. You may receive a message about a successful payment again, but access for this payment must be granted once.
Two separate entries create a problem regardless of the order. If you save the payment first and then extend access, the failure between the steps will leave the user without renewal. If you start with an extension, re-processing may issue it a second time.
I combined the payment recording and subscription issuance into one atomic operation in Redis. The payment ID helps you recognize a repeat and not issue a subscription a second time.
Separately, it was necessary to link each payment with a change in access. When making a refund, this allows you to undo exactly the change that the refunded payment made and keep access from other payments.
I check the logic of subscriptions and refunds with tests on a real Redis engine. Additionally, I used several independent AI agents for the review: they helped me find errors in replays, crash handling, and paid launches. Their findings became the starting point for reproducing and verifying corrections.
7. What checks are useful after the first launch
These cases resulted in a list of scenarios for testing a similar system.:
- Repeat the delivery of one event while the first processing is still in progress: there should be no second parallel dispatch.
- Pass two references to the same object with different service parameters: they must access a single cache entry. The parameters that define the object itself must be saved.
- Separately reproduce the service's response with an error and connection termination: both situations should lead to the intended processing result.
- Check the file without dimensions in the loader's response: you need to get metadata from the file itself or explicitly process their absence.
- Run the script almost at the time limit of the function: an external paid task should not start if there is not enough time for its expected completion and sending the result.
- Repeat the payment event and verify the refund: access should only change according to the specific payment.
The first successful launch confirmed that the main scenario was working. An analysis of repetitions, failures, and intermediate states showed what else needs to be checked before relying on it in daily work.
В своём проекте я автоматизировал простой сценарий: обработать ссылку из сообщения Telegram, получить медиафайл и отправить его в тот же чат. Первую рабочую версию собрал меньше чем за день. После запуска началась работа с менее очевидными ситуациями: повторной доставкой событий, ошибками внешних сервисов и ограниченным временем выполнения.
Ниже — разбор конкретных ошибок, их причин и способов проверки. Эти ситуации могут встретиться и в других приложениях, которые принимают вебхуки, используют кэш и обращаются к платным API.
1. Архитектура и границы выполнения
Для старта выбрал Python, FastAPI и yt-dlp. Бот работает через вебхук: Telegram присылает событие, функция на Vercel обрабатывает ссылку и отправляет файл в чат. Постоянно работающий сервер для этого сценария не понадобился.
Одна операция включала несколько этапов: распознать ссылку, проверить кэш, получить файл и отправить результат. Все этапы должны были уложиться во время, выделенное серверной функции.
Но успешная загрузка на моём компьютере ещё не означала, что та же ссылка сработает после деплоя. YouTube и Instagram могли ограничивать запросы с IP-адреса датацентра. У разных типов публикаций тоже оказалось разное поведение: ролик скачивался, а фото или карусель — уже нет.
Пришлось выстроить несколько способов получения контента. Бот начинает с прямого запроса, а для отдельных случаев использует дополнительные варианты, включая платный сервис Apify. Каждый такой вариант добавляет время ожидания, вероятность ошибки и иногда расходы.
2. Надёжность начинается с повторов и неожиданных данных
Первый заметный баг выглядел просто: одно видео иногда приходило дважды. Пока бот долго скачивал файл, Telegram повторно доставлял событие, и обработка запускалась ещё раз. Ссылку нужно было помечать как находящуюся в работе сразу, до скачивания.
Потом добавил кэш. После отправки файла Telegram возвращает его идентификатор, который можно использовать повторно. Сохранил эти идентификаторы в Redis: знакомую ссылку больше не нужно каждый раз скачивать и загружать заново.
Однако кэш почти не срабатывал. Кнопка «Поделиться» добавляла к ссылкам служебные параметры, поэтому один ролик от двух людей выглядел как два разных адреса. Помогло приведение ссылок к единому виду перед поиском в кэше.
Ещё один сбой нашёлся на границе с Redis. Код обрабатывал ошибку, которую вернул сервис, но пропускал исключение при обрыве соединения. Из-за этого ссылка могла остаться без ответа. Проверка с реально недоступным адресом показала то, чего не показал тест с условным ответом «500».
С тех пор при проверке интеграций отдельно смотрю, что произойдёт, если сервис ответит ошибкой, не ответит вообще или ответит слишком поздно.
3. Баг, который я исправлял три раза
Вертикальные видео на телефоне иногда отображались квадратными, хотя на компьютере всё выглядело нормально.
Сначала я начал передавать Telegram ширину и высоту ролика. Затем решил, что проблема в превью, и стал генерировать его самостоятельно. Обе версии звучали убедительно, но не помогли.
Причина нашлась, когда я взял конкретную проблемную ссылку и посмотрел на реальные данные: библиотека возвращала пустые значения ширины и высоты. Код «передавал размеры», но передавать было нечего. В итоге стал читать их из самого файла.
Похожие сюрпризы возникали при подключении новых платформ. Например, галерея Reddit могла ходить по кругу между двумя адресами. Каждый отдельный запрос проходил успешно, а вся операция не заканчивалась. Для такого случая нужен предел времени на обработку целиком.
Эти истории изменили мой порядок работы: сначала воспроизвожу проблему и смотрю, что реально приходит и уходит, затем выбираю исправление.
4. Когда техническая ошибка становится расходом
С подключением платных сервисов таймауты стали влиять на деньги.
В одном из сценариев бот запускал платную загрузку, когда до конца доступного времени оставалось около 14 секунд. Самой загрузке могло понадобиться до 50 секунд. Функция завершалась, внешний сервис продолжал работу, а пользователь не получал результат. Повторная доставка события могла запустить ещё один платный запрос.
Теперь перед таким запуском проверяется, достаточно ли осталось времени. Для долгих операций в Mini App использовал другой подход: запрос запускает обработку и возвращает статус, а интерфейс позже запрашивает результат. Уже начатую задачу можно использовать повторно, в том числе если один профиль одновременно открыли несколько человек.
Здесь же появился ещё один принцип: бесплатный для пользователя сценарий всё равно нужно считать в расходах проекта. Для обращений к платным сервисам я ввёл отдельные бюджеты, чтобы активность одного пользователя не создавала неограниченные затраты.
5. Почему счётчик запросов не заменяет бюджет
Обращения к внешним сервисам могут иметь разную стоимость. В моём случае расходы зависели в том числе от длительности обрабатываемого медиа. Поэтому одинаковое число запросов у двух пользователей ещё не означало одинаковые затраты.
Для платной обработки YouTube я стал резервировать бюджет до запуска задачи, а после получения результата пересчитывать расход по фактической длительности. Расходы разных интеграций учитываются отдельно, чтобы одна категория запросов не исчерпывала бюджет другой.
Здесь нашлась ошибка в учёте: сразу после завершения задачи сервис показывал предварительную стоимость, а окончательное списание появлялось позже. Если сразу вернуть разницу между резервом и предварительной суммой, доступный бюджет окажется завышенным.
В этом сценарии перешёл к расчёту по длительности результата и расценкам выбранного обработчика. Для похожей интеграции полезно отдельно выяснить, когда провайдер окончательно фиксирует стоимость, и проверить поведение при задержке этих данных.
Ещё одно решение касалось недоступной базы: если нельзя проверить бюджет, платная операция не запускается. Иначе сбой учёта превращается в возможность бесконтрольно расходовать средства.
6. Как обработать повторное событие об оплате
Подключение платежей через Telegram Stars добавило ещё один случай повторной доставки. Сообщение об успешной оплате может прийти повторно, но доступ по этому платежу должен выдаваться один раз.
Две отдельные записи создают проблему независимо от порядка. Если сначала сохранить платёж, а затем продлить доступ, сбой между шагами оставит пользователя без продления. Если начать с продления, повторная обработка может выдать его второй раз.
Запись платежа и выдачу подписки объединил в одну атомарную операцию в Redis. Идентификатор платежа помогает распознать повтор и не выдать подписку второй раз.
Отдельно понадобилось связать каждый платёж с изменением доступа. При возврате это позволяет отменить именно то изменение, которое внёс возвращаемый платёж, и сохранить доступ от остальных оплат.
Логику подписок и возвратов проверяю тестами на настоящем Redis-движке. Дополнительно использовал несколько независимых ИИ-агентов для ревью: они помогли найти ошибки в повторах, обработке сбоев и платных запусках. Их находки становились отправной точкой для воспроизведения и проверки исправлений.
7. Какие проверки полезны после первого запуска
Из этих случаев получился список сценариев для проверки похожей системы:
- Повторить доставку одного события, пока первая обработка ещё идёт: не должно появиться второй параллельной отправки.
- Передать две ссылки на один объект с разными служебными параметрами: они должны обращаться к одной записи кэша. Параметры, определяющие сам объект, при этом нужно сохранить.
- Отдельно воспроизвести ответ сервиса с ошибкой и обрыв соединения: обе ситуации должны приводить к предусмотренному результату обработки.
- Проверить файл без размеров в ответе загрузчика: метаданные нужно получить из самого файла либо явно обработать их отсутствие.
- Запустить сценарий почти у предела времени функции: внешняя платная задача не должна стартовать, если времени на её ожидаемое завершение и отправку результата недостаточно.
- Повторить событие об оплате и проверить возврат: доступ должен измениться только в соответствии с конкретным платежом.
Первый успешный запуск подтвердил, что основной сценарий работает. Разбор повторов, сбоев и промежуточных состояний показал, что ещё нужно проверить до того, как полагаться на него в повседневной работе.