< Summary

Line coverage
96%
Covered lines: 101
Uncovered lines: 4
Coverable lines: 105
Total lines: 357
Line coverage: 96.1%
Branch coverage
85%
Covered branches: 53
Total branches: 62
Branch coverage: 85.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: .cctor()100%11100%
File 1: .ctor(...)50%44100%
File 1: HandleAsync()89.28%282895.23%
File 1: GetUpdateType()100%22100%
File 1: HandleDecodeFailureAsync()75%161693.93%
File 1: TryGetUpdateId(...)100%66100%
File 1: IsSecretTokenAccepted(...)100%66100%
File 2: .cctor()100%11100%

File(s)

/_/src/TeleFlow.Telegram.Webhooks/Internal/TelegramRawWebhookEndpoint.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.AspNetCore.Http;
 3using Microsoft.Extensions.DependencyInjection;
 4using Microsoft.Extensions.Logging;
 5using TeleFlow.Telegram.Internal;
 6using TeleFlow.Telegram.Schema.Types;
 7
 8namespace TeleFlow.Telegram.Webhooks.Internal;
 9
 10/// <summary>
 11/// Handles raw ASP.NET Core webhook requests, validates Telegram webhook security metadata,
 12/// deserializes updates, and passes accepted updates to the user-provided webhook handler.
 13/// </summary>
 14internal sealed partial class TelegramRawWebhookEndpoint
 15{
 16    private const string SecretTokenHeaderName = "X-Telegram-Bot-Api-Secret-Token";
 17
 18    private const int SecretTokenRejectedEventId = 1;
 19    private const int InvalidPayloadRejectedEventId = 2;
 20    private const int UpdateReceivedEventId = 3;
 21    private const int UpdateProcessedEventId = 4;
 22    private const int UpdateProcessingFailedEventId = 5;
 23    private const int UpdateDecodeStoppedEventId = 6;
 24    private const int UpdateDecodeSkippedEventId = 7;
 25
 126    private static readonly TelegramJsonOptions DefaultJsonOptions = TelegramJsonOptions.CreateDefault();
 27
 28    private readonly TelegramRawWebhookHandler _handler;
 29    private readonly TelegramRawWebhookOptions _options;
 30    private readonly ILogger<TelegramRawWebhookEndpoint>? _logger;
 31
 32    public TelegramRawWebhookEndpoint(
 33        TelegramRawWebhookHandler handler,
 34        TelegramRawWebhookOptions options,
 35        ILogger<TelegramRawWebhookEndpoint>? logger = null)
 36    {
 2737        _handler = handler ?? throw new ArgumentNullException(nameof(handler));
 2738        _options = options ?? throw new ArgumentNullException(nameof(options));
 2739        _logger = logger;
 2740    }
 41
 42    public async Task<IResult> HandleAsync(HttpContext context)
 43    {
 2644        ArgumentNullException.ThrowIfNull(context);
 45
 2646        if (!IsSecretTokenAccepted(context))
 47        {
 548            if (_logger is not null)
 49            {
 550                LogSecretTokenRejected(_logger, _options.SecretTokenFailureStatusCode);
 51            }
 52
 553            return Results.StatusCode(_options.SecretTokenFailureStatusCode);
 54        }
 55
 2156        var jsonOptions = context.RequestServices.GetService<TelegramJsonOptions>() ?? DefaultJsonOptions;
 57        JsonDocument? document;
 58
 59        try
 60        {
 2161            document = await JsonDocument.ParseAsync(
 2162                context.Request.Body,
 2163                cancellationToken: context.RequestAborted).ConfigureAwait(false);
 1764        }
 465        catch (JsonException)
 66        {
 467            if (_logger is not null)
 68            {
 469                LogInvalidPayloadRejected(_logger, _options.InvalidPayloadStatusCode);
 70            }
 71
 472            return Results.StatusCode(_options.InvalidPayloadStatusCode);
 73        }
 74
 1775        using (document)
 76        {
 1777            var payload = document.RootElement;
 1778            if (!TryGetUpdateId(payload, out var updateId))
 79            {
 280                if (_logger is not null)
 81                {
 282                    LogInvalidPayloadRejected(_logger, _options.InvalidPayloadStatusCode);
 83                }
 84
 285                return Results.StatusCode(_options.InvalidPayloadStatusCode);
 86            }
 87
 1588            var decodeResult = TelegramUpdateDecoder.Decode(
 1589                payload,
 1590                updateId,
 1591                jsonOptions.SerializerOptions);
 92
 1593            if (!decodeResult.IsSuccess)
 94            {
 495                return await HandleDecodeFailureAsync(context, decodeResult).ConfigureAwait(false);
 96            }
 97
 1198            var update = decodeResult.Update!;
 1199            string? updateType = null;
 100            string GetUpdateType()
 101            {
 4102                return updateType ??= TelegramWebhookUpdateLogFormatter.GetUpdateType(update);
 103            }
 104
 11105            if (_logger?.IsEnabled(LogLevel.Debug) == true)
 106            {
 1107                LogUpdateReceived(_logger, update.UpdateId, GetUpdateType());
 108            }
 109
 11110            var bot = context.RequestServices.GetRequiredService<ITelegramClient>();
 111            try
 112            {
 11113                var result = await _handler(update, bot, context.RequestAborted).ConfigureAwait(false);
 114
 9115                if (_logger?.IsEnabled(LogLevel.Debug) == true)
 116                {
 1117                    LogUpdateProcessed(_logger, update.UpdateId, GetUpdateType());
 118                }
 119
 9120                return result;
 121            }
 0122            catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 123            {
 0124                throw;
 125            }
 2126            catch (Exception exception)
 127            {
 2128                if (_logger?.IsEnabled(LogLevel.Error) == true)
 129                {
 2130                    LogUpdateProcessingFailed(_logger, exception, update.UpdateId, GetUpdateType());
 131                }
 132
 2133                throw;
 134            }
 135        }
 23136    }
 137
 138    private async Task<IResult> HandleDecodeFailureAsync(
 139        HttpContext context,
 140        TelegramUpdateDecodeResult item)
 141    {
 4142        var exception = item.Exception
 4143            ?? throw new InvalidOperationException("A failed Telegram update decode result did not contain an exception.
 4144        var failure = new TelegramUpdateDecodeFailure(
 4145            TelegramUpdateTransport.Webhook,
 4146            item.UpdateId,
 4147            item.RawPayloadJson
 4148                ?? throw new InvalidOperationException("A failed Telegram update decode result did not contain raw paylo
 4149            item.PayloadSha256
 4150                ?? throw new InvalidOperationException("A failed Telegram update decode result did not contain a payload
 4151            exception);
 4152        var policy = context.RequestServices.GetService<ITelegramUpdateDecodeFailurePolicy>()
 4153            ?? StopTelegramUpdateDecodeFailurePolicy.Instance;
 4154        var decision = await policy
 4155            .DecideAsync(failure, context.RequestAborted)
 4156            .ConfigureAwait(false);
 157
 158        switch (decision)
 159        {
 160            case TelegramUpdateDecodeFailureDecision.Stop:
 1161                if (_logger is not null)
 162                {
 1163                    LogUpdateDecodeStopped(
 1164                        _logger,
 1165                        exception,
 1166                        item.UpdateId,
 1167                        exception.JsonPath,
 1168                        exception.PayloadSha256);
 169                }
 170
 1171                return Results.StatusCode(StatusCodes.Status500InternalServerError);
 172            case TelegramUpdateDecodeFailureDecision.Skip:
 2173                if (_logger is not null)
 174                {
 2175                    LogUpdateDecodeSkipped(
 2176                        _logger,
 2177                        item.UpdateId,
 2178                        exception.JsonPath,
 2179                        exception.PayloadSha256);
 180                }
 181
 2182                return Results.Ok();
 183            default:
 0184                throw new InvalidOperationException(
 0185                    $"Unsupported Telegram update decode failure decision '{decision}'.");
 186        }
 3187    }
 188
 189    private static bool TryGetUpdateId(JsonElement payload, out long updateId)
 190    {
 17191        updateId = default;
 17192        return payload.ValueKind == JsonValueKind.Object &&
 17193            payload.TryGetProperty("update_id", out var updateIdElement) &&
 17194            updateIdElement.ValueKind == JsonValueKind.Number &&
 17195            updateIdElement.TryGetInt64(out updateId);
 196    }
 197
 198    private bool IsSecretTokenAccepted(HttpContext context)
 199    {
 26200        if (_options.SecretToken is null)
 201        {
 18202            return true;
 203        }
 204
 8205        return context.Request.Headers.TryGetValue(SecretTokenHeaderName, out var values) &&
 8206            values.Count == 1 &&
 8207            string.Equals(values[0], _options.SecretToken, StringComparison.Ordinal);
 208    }
 209
 210    [LoggerMessage(
 211        EventId = SecretTokenRejectedEventId,
 212        Level = LogLevel.Warning,
 213        Message = "Telegram webhook request rejected because secret token validation failed. status={StatusCode}.")]
 214    private static partial void LogSecretTokenRejected(
 215        ILogger logger,
 216        int statusCode);
 217
 218    [LoggerMessage(
 219        EventId = InvalidPayloadRejectedEventId,
 220        Level = LogLevel.Warning,
 221        Message = "Telegram webhook request rejected because payload was invalid. status={StatusCode}.")]
 222    private static partial void LogInvalidPayloadRejected(
 223        ILogger logger,
 224        int statusCode);
 225
 226    [LoggerMessage(
 227        EventId = UpdateReceivedEventId,
 228        Level = LogLevel.Debug,
 229        Message = "Telegram webhook update received. update_id={UpdateId}, type={UpdateType}.")]
 230    private static partial void LogUpdateReceived(
 231        ILogger logger,
 232        long updateId,
 233        string updateType);
 234
 235    [LoggerMessage(
 236        EventId = UpdateProcessedEventId,
 237        Level = LogLevel.Debug,
 238        Message = "Telegram webhook update processed. update_id={UpdateId}, type={UpdateType}.")]
 239    private static partial void LogUpdateProcessed(
 240        ILogger logger,
 241        long updateId,
 242        string updateType);
 243
 244    [LoggerMessage(
 245        EventId = UpdateProcessingFailedEventId,
 246        Level = LogLevel.Error,
 247        Message = "Telegram webhook update processing failed. update_id={UpdateId}, type={UpdateType}.")]
 248    private static partial void LogUpdateProcessingFailed(
 249        ILogger logger,
 250        Exception exception,
 251        long updateId,
 252        string updateType);
 253
 254    [LoggerMessage(
 255        EventId = UpdateDecodeStoppedEventId,
 256        Level = LogLevel.Error,
 257        Message = "Telegram webhook update decoding failed and the request was rejected for retry. update_id={UpdateId},
 258    private static partial void LogUpdateDecodeStopped(
 259        ILogger logger,
 260        Exception exception,
 261        long updateId,
 262        string? jsonPath,
 263        string payloadSha256);
 264
 265    [LoggerMessage(
 266        EventId = UpdateDecodeSkippedEventId,
 267        Level = LogLevel.Warning,
 268        Message = "Telegram webhook update decoding failed and the update was acknowledged by application policy. update
 269    private static partial void LogUpdateDecodeSkipped(
 270        ILogger logger,
 271        long updateId,
 272        string? jsonPath,
 273        string payloadSha256);
 274}

/_/src/TeleFlow.Telegram.Webhooks/obj/Release/net10.0/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs

File '/_/src/TeleFlow.Telegram.Webhooks/obj/Release/net10.0/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs' does not exist (any more).