< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Callback.InMemoryCallbackDispatchWorker
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/InMemoryCallbackDispatchWorker.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
52%
Covered lines: 32
Uncovered lines: 29
Coverable lines: 61
Total lines: 154
Line coverage: 52.4%
Branch coverage
59%
Covered branches: 13
Total branches: 22
Branch coverage: 59%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/02/2026 - 00:16:25 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/23/2026 - 00:12:18 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d901/24/2026 - 19:35:59 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/03/2026 - 18:01:44 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe 01/02/2026 - 00:16:25 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/12/2026 - 22:21:01 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@91f4d7da7b838286aa6f574ec486cbb057167d6401/23/2026 - 00:12:18 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@67ed8a99376189d7ed94adba1b1854518edd75d901/24/2026 - 19:35:59 Line coverage: 55.7% (34/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@f59dcba478ea75f69584d696e5f1fb1cfa40aa5102/03/2026 - 18:01:44 Line coverage: 52.4% (32/61) Branch coverage: 59% (13/22) Total lines: 154 Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%2257.14%
DispatchOneAsync()68.75%311660.86%
SendCallbackAsync()100%2118.75%
Snip(...)0%2040%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/InMemoryCallbackDispatchWorker.cs

#LineLine coverage
 1using System.Net.Http.Headers;
 2using System.Net.Sockets;
 3using System.Security.Authentication;
 4
 5namespace Kestrun.Callback;
 6
 7/// <summary>
 8/// In-memory implementation of <see cref="ICallbackDispatcher"/>.
 9/// Enqueues callback requests into an in-memory queue for processing.
 10/// </summary>
 11/// <remarks>
 12/// Initializes a new instance of the <see cref="InMemoryCallbackDispatchWorker"/> class.
 13/// </remarks>
 14/// <param name="queue">The in-memory callback queue.</param>
 15/// <param name="httpClientFactory">The HTTP client factory.</param>
 16/// <param name="log">The logger instance.</param>
 117public sealed class InMemoryCallbackDispatchWorker(
 118    InMemoryCallbackQueue queue,
 119    IHttpClientFactory httpClientFactory,
 120    Serilog.ILogger log) : BackgroundService
 21{
 122    private readonly InMemoryCallbackQueue _queue = queue;
 123    private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
 124    private readonly Serilog.ILogger _log = log;
 25
 26    /// <summary>
 27    /// Executes the callback dispatching process.
 28    /// </summary>
 29    /// <param name="stoppingToken">The token to monitor for cancellation requests.</param>
 30    /// <returns>A task that represents the asynchronous operation.</returns>
 31    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 32    {
 133        var httpClient = _httpClientFactory.CreateClient("kestrun-callbacks");
 34
 235        while (!stoppingToken.IsCancellationRequested)
 36        {
 37            CallbackRequest req;
 38            try
 39            {
 140                req = await _queue.Channel.Reader.ReadAsync(stoppingToken).ConfigureAwait(false);
 141            }
 042            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
 43            {
 044                break;
 45            }
 46
 47            try
 48            {
 149                await DispatchOneAsync(httpClient, req, stoppingToken).ConfigureAwait(false);
 150            }
 051            catch (Exception ex)
 52            {
 053                _log.Error(ex, "Callback dispatch failed. CallbackId={CallbackId} Url={Url}",
 054                    req.CallbackId, req.TargetUrl);
 55
 56                // TODO: retry / dead-letter policy
 057            }
 158        }
 159    }
 60
 61    /// <summary>
 62    /// Dispatches a single callback request.
 63    /// </summary>
 64    /// <param name="httpClient">The HTTP client used to send the request.</param>
 65    /// <param name="req">The callback request to be dispatched.</param>
 66    /// <param name="token">The cancellation token to observe.</param>
 67    /// <returns>A task that represents the asynchronous operation.</returns>
 68    private async Task DispatchOneAsync(HttpClient httpClient, CallbackRequest req, CancellationToken token)
 69    {
 170        using var msg = new HttpRequestMessage(new HttpMethod(req.HttpMethod), req.TargetUrl);
 71
 172        if (req.Body is not null)
 73        {
 174            msg.Content = new ByteArrayContent(req.Body);
 75            // Set Content-Type explicitly (avoids overload confusion)
 176            if (!string.IsNullOrWhiteSpace(req.ContentType))
 77            {
 178                msg.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(req.ContentType);
 79            }
 80        }
 81
 482        foreach (var h in req.Headers)
 83        {
 84            // Content headers must go on Content, not on msg.Headers
 185            if (!msg.Headers.TryAddWithoutValidation(h.Key, h.Value))
 86            {
 087                _ = (msg.Content?.Headers.TryAddWithoutValidation(h.Key, h.Value));
 88            }
 89        }
 90
 191        _log.Information("Sending callback. CallbackId={CallbackId} Url={Url}", req.CallbackId, req.TargetUrl);
 92
 193        using var resp = await SendCallbackAsync(httpClient, msg, token).ConfigureAwait(false);
 94
 195        if ((int)resp.StatusCode >= 500)
 96        {
 097            var body = await resp.Content.ReadAsStringAsync(token).ConfigureAwait(false);
 098            _log.Warning("Callback got {StatusCode} (server error). CallbackId={CallbackId} Url={Url} BodySnippet={Body}
 099                (int)resp.StatusCode, req.CallbackId, req.TargetUrl, Snip(body, 500));
 100
 101            // TODO: retry
 0102            return;
 103        }
 104
 1105        if ((int)resp.StatusCode >= 400)
 106        {
 0107            var body = await resp.Content.ReadAsStringAsync(token).ConfigureAwait(false);
 0108            _log.Warning("Callback rejected {StatusCode}. CallbackId={CallbackId} Url={Url} BodySnippet={Body}",
 0109                (int)resp.StatusCode, req.CallbackId, req.TargetUrl, Snip(body, 500));
 110
 111            // TODO: dead-letter (usually)
 0112            return;
 113        }
 114
 1115        _log.Information("Callback delivered. CallbackId={CallbackId} Status={StatusCode}",
 1116            req.CallbackId, (int)resp.StatusCode);
 1117    }
 118
 119    private async Task<HttpResponseMessage> SendCallbackAsync(
 120        HttpClient httpClient,
 121        HttpRequestMessage request,
 122        CancellationToken token)
 123    {
 124        try
 125        {
 1126            return await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token)
 1127                                   .ConfigureAwait(false);
 128        }
 0129        catch (TaskCanceledException ex)
 130        {
 0131            _log.Warning(ex, "Callback timed out. Url={Url}", request.RequestUri);
 0132            throw;
 133        }
 0134        catch (HttpRequestException ex) when (ex.InnerException is SocketException se)
 135        {
 0136            _log.Error(ex, "Callback DNS/connect failure. SocketError={SocketError} Url={Url}",
 0137                se.SocketErrorCode, request.RequestUri);
 0138            throw;
 139        }
 0140        catch (HttpRequestException ex) when (ex.InnerException is AuthenticationException or IOException)
 141        {
 0142            _log.Error(ex, "Callback TLS/SSL failure. Url={Url}", request.RequestUri);
 0143            throw;
 144        }
 0145        catch (HttpRequestException ex)
 146        {
 0147            _log.Error(ex, "Callback HTTP request failure. Url={Url}", request.RequestUri);
 0148            throw;
 149        }
 1150    }
 151
 152    private static string Snip(string? s, int max)
 0153        => string.IsNullOrEmpty(s) ? "" : (s.Length <= max ? s : s[..max]);
 154}