Skip to content

Контракт Ошибок И Подтверждения Update

Эта страница описывает, что происходит после того, как incoming update попал во framework: какие исходы подтверждают update, а какие намеренно возвращаются в hosting environment как ошибка.

Коротко: TeleFlow подтверждает update только после явного успешного исхода. Он не пытается угадать, что упавшая бизнес-операция безопасна для игнорирования.

Зачем Нужен Этот Контракт

Telegram может повторно доставить update, если предыдущая доставка не была подтверждена. Это полезно: временное падение процесса не должно молча потерять действие пользователя. Но если handler уже изменил базу, списал баланс или отправил сообщение, повторная доставка может повторить side effect.

Поэтому TeleFlow даёт at-least-once delivery, а не exactly-once delivery. Приложение, меняющее durable state, само отвечает за idempotency бизнес-операций в местах, где возможна повторная доставка.

Pipeline Framework

Для framework update путь выглядит так:

  1. Transport получает update.
  2. IUpdateProcessor создаёт scoped UpdateContext и запускает update middleware.
  3. Telegram dispatcher выбирает route и вызывает его handler.
  4. Для callback route TeleFlow после успешного handler запускает настроенный automatic callback answer.
  5. Если route execution упал, вызываются compatible [Error] и [Error<TException>] handlers.
  6. Transport подтверждает update только после успешного завершения processor.

Выбранный handler и automatic callback answer образуют одну границу route execution. Auto-answer выполняется после handler, но это не повод обходить то же recovery-решение, которое действует для выбранного route.

Матрица Исходов

Исход Результат framework Long polling Webhook
Для update нет подходящего route Нормальное завершение Offset продвигается 200 OK
Rate limiter намеренно отклонил update Нормальное завершение Offset продвигается 200 OK
Handler и automatic callback answer успешно завершились Нормальное завершение Offset продвигается 200 OK
[Error] handler вернул Handled Нормальное завершение Offset продвигается 200 OK
Error handler вернул Unhandled, и дальше никто не обработал ошибку Ошибка идёт выше Offset не продвигается Ошибка endpoint идёт выше
Handler, middleware или error handler бросил exception Ошибка идёт выше Offset не продвигается Ошибка endpoint идёт выше
Application cancellation Обработка останавливается Offset не продвигается Работает cancellation semantics
Валидный update не соответствует установленной schema, а decode policy вернула Stop Обработка не начинается Offset не продвигается; polling останавливается без retry детерминированной ошибки 500 Internal Server Error
Decode policy durable-сохранила update в quarantine и вернула Skip Обработка не начинается Offset продвигается через этот update 200 OK
Decode policy бросила exception или была отменена Ошибка идёт выше Offset не продвигается Ошибка endpoint идёт выше

Для webhook TeleFlow не подделывает успешный response после unhandled failure. ASP.NET Core возвращает failure response, а решение о повторной доставке остаётся у механизма доставки Telegram.

Ошибки Декодирования Telegram Schema

Long polling и webhooks передают синтаксически валидные Telegram updates в одну ITelegramUpdateDecodeFailurePolicy. Default policy возвращает Stop. TeleFlow не считает update одноразовым только потому, что установленная schema не смогла его понять.

Приложение, для которого availability важнее остановки, может заменить policy:

public sealed class DurableQuarantinePolicy(IUpdateQuarantineStore store)
    : ITelegramUpdateDecodeFailurePolicy
{
    public async ValueTask<TelegramUpdateDecodeFailureDecision> DecideAsync(
        TelegramUpdateDecodeFailure failure,
        CancellationToken ct)
    {
        await store.UpsertAsync(
            failure.UpdateId,
            failure.Transport,
            failure.RawPayloadJson,
            failure.PayloadSha256,
            failure.Exception.JsonPath,
            ct);

        return TelegramUpdateDecodeFailureDecision.Skip;
    }
}

services.AddTelegramUpdateDecodeFailurePolicy<DurableQuarantinePolicy>();

Skip явно подтверждает потерю update. Возвращай его только после commit raw payload и diagnostics в durable storage. Если quarantine storage недоступен, бросай exception или возвращай Stop: transport не подтвердит update. Policy может быть вызвана повторно до следующего polling request, который подтвердит новый offset, поэтому запись должна быть idempotent по bot identity и update_id.

Raw payload может содержать текст сообщения, данные пользователя, платёжные данные или callback content. TeleFlow никогда не пишет его в логи. Application quarantine должна иметь подходящие access control и retention.

Невалидный webhook JSON и payload без корректного update_id считаются невалидными запросами и не попадают в эту policy. Прямой generated-вызов вроде ITelegramClient.SendAsync(new GetUpdates()) тоже остаётся строгим и атомарным; изоляция отдельных updates принадлежит long-polling и webhook transport paths.

Error Handlers Это Явная Точка Recovery

Используй error handler, когда приложение действительно приняло решение о recovery. Возврат Handled означает: этот update дошёл до durable и приемлемого результата, transport может его подтвердить.

public sealed class KnownErrors
{
    [Error<RejectedUserActionException>]
    public async Task<TelegramErrorHandlingResult> RejectedAction(
        MessageContext ctx,
        RejectedUserActionException exception,
        CancellationToken ct)
    {
        await ctx.Message.AnswerAsync(exception.Message, ct);
        return TelegramErrorHandlingResult.Handled;
    }
}

Unhandled просит TeleFlow попробовать следующий compatible error handler. Если error handler сам бросил exception, recovery не удался: новая ошибка остаётся видимой transport-у и host-у.

Не регистрируй catch-all [Error], который возвращает Handled на любой exception. Иначе defects, failed invariants и сломанная конфигурация превратятся в подтверждённые updates без честного пути восстановления.

Ошибки Telegram API

Outgoing Telegram request, сделанный внутри handler, является частью route execution. Его ошибку можно обработать compatible error handler-ом или оставить unhandled, сохранив at-least-once delivery.

Не считай любой TelegramBadRequestException безопасным. 400 может означать ожидаемый no-op вроде unchanged message, но может говорить о невалидной разметке, неверном chat или баге приложения. Предпочитай один из путей:

  • предотврати известный no-op до отправки запроса;
  • переведи известное состояние приложения в domain exception и обработай именно его;
  • оставь неизвестную ошибку Telegram request видимой и неподтверждённой.

Retry Telegram requests тоже намеренно узкий. Client автоматически учитывает bounded 429 retry_after. Raw getUpdates имеет собственный transient backoff, потому что это idempotent read. TeleFlow не retry-ит вслепую все outgoing network или 5xx failures: Telegram мог уже выполнить write до того, как client потерял response.

Ошибки response envelope и schema decode детерминированы и не считаются transient polling failures. Повторная десериализация тех же байтов с exponential backoff не исправляет устаревшую schema.

Middleware Это Другая Граница

Update middleware выполняется до dispatch или вокруг него. Middleware может намеренно остановить update, не вызвав next(context); так работает rate-limit rejection, и это нормальное завершение.

Exception из middleware не является ошибкой выбранного Telegram route, поэтому не попадает в [Error] handlers. Это намеренно. Middleware часто владеет authentication, storage transactions, tenancy или глобальными safeguards; handler-level recovery не может честно заявить, что такая ошибка успешно завершилась.

Лови и решай ошибку внутри middleware только когда само middleware может принять durable и явное решение.

Automatic Callback Answers

Automatic callback answer включается только явно. Обычный callback handler не посылает answerCallbackQuery, если на методе или типе нет [AutoAnswerCallback] и приложение не зарегистрировало AddAutoCallbackAnswer(...). Если не задано ни одно из этих двух условий, TeleFlow не отправляет автоматический callback answer.

[AutoAnswerCallback] и AddAutoCallbackAnswer(...) работают только после успешного выбранного callback handler. Если автоматический answerCallbackQuery падает, TeleFlow использует тот же [Error] pipeline, что и для выбранного route.

Так не появляется отдельное скрытое правило для framework action:

  • приложение может вернуть Handled, когда явно принимает этот исход;
  • иначе ошибка остаётся unhandled, и update не подтверждается;
  • ошибка automatic callback answer не проглатывается молча.

Рекомендации Для Production

Держи handlers тонкими, а durable state changes выноси в application services с явными idempotency rules. В частности:

  • сохраняй deduplication key до необратимой работы, когда это нужно бизнесу;
  • используй database transaction или outbox, когда state и external effects должны оставаться согласованными;
  • явно обрабатывай известные user-facing rejections;
  • оставляй неизвестные failures логам, telemetry и host-у;
  • используй process supervisor для long-polling worker, но не считай restart бизнесовой retry policy.

Это сложнее, чем подтверждать каждый exception, но именно так не теряются действия пользователя молча.

Связанные Страницы