< Summary

Information
Class: TeleFlow.Telegram.HttpClientTelegramTransport
Assembly: TeleFlow.Telegram.Client
File(s): /_/src/TeleFlow.Telegram.Client/HttpClientTelegramTransport.cs
Line coverage
85%
Covered lines: 64
Uncovered lines: 11
Coverable lines: 75
Total lines: 210
Line coverage: 85.3%
Branch coverage
85%
Covered branches: 17
Total branches: 20
Branch coverage: 85%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%11100%
CreateOwned(...)100%11100%
SendAsync()50%22100%
Dispose()100%44100%
BuildContent(...)75%4471.42%
BuildMultipartContent(...)83.33%6688.88%
CopyHeaders(...)100%44100%
.ctor(...)100%11100%
get_CanRead()100%11100%
get_CanSeek()100%11100%
get_CanWrite()100%210%
get_Length()100%11100%
get_Position()100%11100%
set_Position(...)100%210%
Flush()100%210%
Read(...)100%210%
Read(...)100%210%
ReadAsync(...)100%11100%
Seek(...)100%210%
SetLength(...)100%210%
Write(...)100%210%
Dispose(...)100%11100%

File(s)

/_/src/TeleFlow.Telegram.Client/HttpClientTelegramTransport.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Net.Http.Headers;
 3using System.Text;
 4
 5namespace TeleFlow.Telegram;
 6
 7/// <summary>
 8/// Default <see cref="ITelegramTransport"/> implementation that sends Telegram Bot API requests through <see cref="Http
 9/// It is used by the generated client pipeline unless an application registers a custom transport.
 10/// </summary>
 11public sealed class HttpClientTelegramTransport : ITelegramTransport, IDisposable
 12{
 13    private readonly HttpClient _httpClient;
 14    private readonly bool _ownsHttpClient;
 15    private bool _disposed;
 16
 17    public HttpClientTelegramTransport(HttpClient httpClient)
 218        : this(httpClient, ownsHttpClient: false)
 19    {
 220    }
 21
 22    private HttpClientTelegramTransport(HttpClient httpClient, bool ownsHttpClient)
 23    {
 28824        ArgumentNullException.ThrowIfNull(httpClient);
 28825        _httpClient = httpClient;
 28826        _ownsHttpClient = ownsHttpClient;
 28827    }
 28
 29    internal static HttpClientTelegramTransport CreateOwned(HttpClient httpClient)
 30    {
 28631        return new HttpClientTelegramTransport(httpClient, ownsHttpClient: true);
 32    }
 33
 34    public async Task<TelegramTransportResponse> SendAsync(
 35        TelegramTransportRequest request,
 36        CancellationToken cancellationToken = default)
 37    {
 7638        ObjectDisposedException.ThrowIf(_disposed, this);
 7539        ArgumentNullException.ThrowIfNull(request);
 40
 7541        using var message = new HttpRequestMessage(HttpMethod.Post, request.Uri)
 7542        {
 7543            Content = BuildContent(request.Content)
 7544        };
 45
 46        try
 47        {
 7548            using var response = await _httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
 7149            var body = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
 50
 7151            return TelegramTransportResponse.FromOwnedBytes(
 7152                (int)response.StatusCode,
 7153                body,
 7154                CopyHeaders(response));
 55        }
 156        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 57        {
 158            throw;
 59        }
 360        catch (HttpRequestException exception)
 61        {
 362            throw new TelegramNetworkException(
 363                $"Telegram request '{request.MethodName}' failed because the HTTP transport failed.",
 364                exception,
 365                request.MethodName,
 366                exception.StatusCode is null ? null : (int)exception.StatusCode);
 67        }
 7168    }
 69
 70    public void Dispose()
 71    {
 28172        if (_disposed)
 73        {
 174            return;
 75        }
 76
 28077        _disposed = true;
 78
 28079        if (_ownsHttpClient)
 80        {
 27881            _httpClient.Dispose();
 82        }
 28083    }
 84
 85    private static HttpContent BuildContent(TelegramTransportContent content)
 86    {
 7587        return content switch
 7588        {
 6889            TelegramJsonTransportContent json => new StringContent(json.Json, Encoding.UTF8, "application/json"),
 790            TelegramMultipartTransportContent multipart => BuildMultipartContent(multipart),
 091            _ => throw new InvalidOperationException(
 092                $"Unsupported Telegram transport content type '{content.GetType().FullName}'.")
 7593        };
 94    }
 95
 96    [SuppressMessage(
 97        "Reliability",
 98        "CA2000:Dispose objects before losing scope",
 99        Justification = "Part content ownership is transferred to MultipartFormDataContent, which is disposed by the req
 100    private static MultipartFormDataContent BuildMultipartContent(TelegramMultipartTransportContent multipart)
 101    {
 7102        var content = new MultipartFormDataContent();
 103
 34104        foreach (var field in multipart.Fields)
 105        {
 10106            content.Add(new StringContent(field.Value, Encoding.UTF8), field.Name);
 107        }
 108
 28109        foreach (var file in multipart.Files)
 110        {
 7111            var streamContent = new StreamContent(new NonDisposingReadStream(file.Content));
 7112            if (!string.IsNullOrWhiteSpace(file.ContentType))
 113            {
 0114                streamContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
 115            }
 116
 7117            content.Add(streamContent, file.Name, file.FileName);
 118        }
 119
 7120        return content;
 121    }
 122
 123    private static Dictionary<string, IReadOnlyList<string>> CopyHeaders(HttpResponseMessage response)
 124    {
 71125        var headers = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
 126
 150127        foreach (var header in response.Headers)
 128        {
 4129            headers[header.Key] = header.Value.ToArray();
 130        }
 131
 426132        foreach (var header in response.Content.Headers)
 133        {
 142134            headers[header.Key] = header.Value.ToArray();
 135        }
 136
 71137        return headers;
 138    }
 139
 140    private sealed class NonDisposingReadStream : Stream
 141    {
 142        [SuppressMessage(
 143            "Usage",
 144            "CA2213:Disposable fields should be disposed",
 145            Justification = "This wrapper deliberately does not own the caller-provided InputFile stream.")]
 146        private readonly Stream _inner;
 147
 7148        public NonDisposingReadStream(Stream inner)
 149        {
 7150            ArgumentNullException.ThrowIfNull(inner);
 7151            _inner = inner;
 7152        }
 153
 7154        public override bool CanRead => _inner.CanRead;
 155
 28156        public override bool CanSeek => _inner.CanSeek;
 157
 0158        public override bool CanWrite => false;
 159
 14160        public override long Length => _inner.Length;
 161
 162        public override long Position
 163        {
 14164            get => _inner.Position;
 0165            set => _inner.Position = value;
 166        }
 167
 168        public override void Flush()
 169        {
 0170        }
 171
 172        public override int Read(byte[] buffer, int offset, int count)
 173        {
 0174            return _inner.Read(buffer, offset, count);
 175        }
 176
 177        public override int Read(Span<byte> buffer)
 178        {
 0179            return _inner.Read(buffer);
 180        }
 181
 182        public override ValueTask<int> ReadAsync(
 183            Memory<byte> buffer,
 184            CancellationToken cancellationToken = default)
 185        {
 14186            return _inner.ReadAsync(buffer, cancellationToken);
 187        }
 188
 189        public override long Seek(long offset, SeekOrigin origin)
 190        {
 0191            return _inner.Seek(offset, origin);
 192        }
 193
 194        public override void SetLength(long value)
 195        {
 0196            throw new NotSupportedException();
 197        }
 198
 199        public override void Write(byte[] buffer, int offset, int count)
 200        {
 0201            throw new NotSupportedException();
 202        }
 203
 204        protected override void Dispose(bool disposing)
 205        {
 206            // Stream ownership stays with the caller-provided InputFile.
 7207            base.Dispose(disposing);
 7208        }
 209    }
 210}