< Summary

Information
Class: Services.ApiServices.BaseApiClient
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/BaseApiClient.cs
Line coverage
16%
Covered lines: 22
Uncovered lines: 115
Coverable lines: 137
Total lines: 331
Line coverage: 16%
Branch coverage
12%
Covered branches: 4
Total branches: 32
Branch coverage: 12.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)50%44100%
MakeRequestPost(...)0%620%
MakeRequestPostAsync()0%132110%
MakeRequestGetAsync()0%132110%
SetRequestHeaders(...)100%210%
SetHttpClientHeaders(...)100%210%
HandleError(...)0%620%
HandleErrorAsync()100%2275%
GetRetryPolicy()100%210%
GetYandexRetryPolicy()100%11100%
GetInteractiveRetryPolicy()100%210%
GetCircuitBreakerPolicy()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/BaseApiClient.cs

#LineLine coverage
 1#pragma warning disable SYSLIB0014 // HttpWebRequest legacy API clients
 2using System.Net;
 3using System.Net.Http;
 4using System.Text;
 5using Microsoft.Extensions.Http;
 6using Newtonsoft.Json;
 7using Newtonsoft.Json.Linq;
 8using Polly;
 9using Polly.Extensions.Http;
 10using SlqStudio.Application.Services.EmailService;
 11
 12namespace Services.ApiServices;
 13
 14/// <summary>
 15/// Базовый класс для всех API клиентов с поддержкой Polly политик и Email уведомлений об ошибках
 16/// </summary>
 17public abstract class BaseApiClient : IApiClient
 18{
 19    // Fallback для кода, созданного вне DI (тесты, legacy-утилиты).
 20    // Thread-safe: HttpClient является потокобезопасным для конкурентных запросов.
 021    protected static readonly HttpClient _fallbackClient = new(new SocketsHttpHandler
 022    {
 023        PooledConnectionLifetime = TimeSpan.FromMinutes(5)
 024    });
 25
 26    protected readonly IHttpClientFactory? _httpClientFactory;
 27    protected readonly IEmailService? _emailService;
 28    protected readonly string _clientName;
 29    protected readonly TimeSpan _timeout;
 30
 5931    protected BaseApiClient(
 5932        IHttpClientFactory? httpClientFactory,
 5933        IEmailService? emailService,
 5934        string clientName,
 5935        TimeSpan? timeout = null)
 36    {
 5937        _httpClientFactory = httpClientFactory;
 5938        _emailService = emailService;
 5939        _clientName = clientName ?? throw new ArgumentNullException(nameof(clientName));
 5940        _timeout = timeout ?? TimeSpan.FromSeconds(15);
 5941    }
 42
 43    /// <summary>
 44    /// Выполняет POST запрос с обработкой ошибок через Polly и отправкой уведомлений
 45    /// </summary>
 46    public virtual JObject MakeRequestPost<T>(T requestModel, string requestUri) where T : IRequestModel
 47    {
 48        try
 49        {
 050            var requestBody = JsonConvert.SerializeObject(requestModel);
 051            var request = (HttpWebRequest)WebRequest.Create(requestUri);
 052            request.Method = "POST";
 053            request.ContentType = "application/json";
 054            request.Timeout = (int)_timeout.TotalMilliseconds;
 55
 56            // Устанавливаем заголовки через виртуальный метод
 057            SetRequestHeaders(request);
 58
 059            using (var streamWriter = new StreamWriter(request.GetRequestStream()))
 60            {
 061                streamWriter.Write(requestBody);
 062                streamWriter.Flush();
 063            }
 64
 065            using (var response = (HttpWebResponse)request.GetResponse())
 66            {
 067                using (var streamReader = new StreamReader(response.GetResponseStream()))
 68                {
 069                    var jsonResponse = streamReader.ReadToEnd();
 070                    if (string.IsNullOrWhiteSpace(jsonResponse))
 71                    {
 072                        return new JObject();
 73                    }
 074                    return JObject.Parse(jsonResponse);
 75                }
 76            }
 77        }
 078        catch (WebException ex) when (ex.Response is HttpWebResponse httpResponse)
 79        {
 080            return HandleError($"HTTP ошибка при запросе к {_clientName}", ex, requestUri);
 81        }
 082        catch (System.Net.Sockets.SocketException ex)
 83        {
 084            return HandleError($"Ошибка соединения с {_clientName}", ex, requestUri);
 85        }
 086        catch (TaskCanceledException ex)
 87        {
 088            return HandleError($"Таймаут при запросе к {_clientName}", ex, requestUri);
 89        }
 090        catch (JsonException ex)
 91        {
 092            return HandleError($"Ошибка парсинга JSON от {_clientName}", ex, requestUri);
 93        }
 094        catch (Exception ex)
 95        {
 096            return HandleError($"Неожиданная ошибка при запросе к {_clientName}", ex, requestUri);
 97        }
 098    }
 99
 100    /// <summary>
 101    /// Выполняет асинхронный POST запрос с использованием HttpClient и Polly
 102    /// </summary>
 103    public virtual async Task<string> MakeRequestPostAsync<T>(T requestModel, string requestUri) where T : IRequestModel
 104    {
 105        try
 106        {
 0107            var client = _httpClientFactory != null
 0108                ? _httpClientFactory.CreateClient(_clientName)
 0109                : _fallbackClient;
 110
 0111            client.Timeout = _timeout;
 0112            SetHttpClientHeaders(client);
 113
 0114            var jsonContent = JsonConvert.SerializeObject(requestModel);
 0115            using var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
 0116            using var httpResponse = await client.PostAsync(requestUri, httpContent);
 0117            var jsonResponse = await httpResponse.Content.ReadAsStringAsync();
 118
 0119            if (!httpResponse.IsSuccessStatusCode)
 120            {
 0121                await HandleErrorAsync($"HTTP ошибка {httpResponse.StatusCode} при запросе к {_clientName}",
 0122                    new HttpRequestException($"Status: {httpResponse.StatusCode}, Response: {jsonResponse}"),
 0123                    requestUri);
 0124                return string.Empty;
 125            }
 126
 0127            return jsonResponse;
 128        }
 129        catch (HttpRequestException ex)
 130        {
 0131            await HandleErrorAsync($"HTTP ошибка при запросе к {_clientName}", ex, requestUri);
 0132            return string.Empty;
 133        }
 134        catch (TaskCanceledException ex)
 135        {
 0136            await HandleErrorAsync($"Таймаут при запросе к {_clientName}", ex, requestUri);
 0137            return string.Empty;
 138        }
 139        catch (System.Net.Sockets.SocketException ex)
 140        {
 0141            await HandleErrorAsync($"Ошибка соединения с {_clientName}", ex, requestUri);
 0142            return string.Empty;
 143        }
 144        catch (Exception ex)
 145        {
 0146            await HandleErrorAsync($"Неожиданная ошибка при запросе к {_clientName}", ex, requestUri);
 0147            return string.Empty;
 148        }
 0149    }
 150
 151    /// <summary>
 152    /// Выполняет асинхронный GET запрос с использованием HttpClient и Polly
 153    /// </summary>
 154    public virtual async Task<string> MakeRequestGetAsync(string requestUri)
 155    {
 156        try
 157        {
 0158            var client = _httpClientFactory != null
 0159                ? _httpClientFactory.CreateClient(_clientName)
 0160                : _fallbackClient;
 161
 0162            client.Timeout = _timeout;
 0163            SetHttpClientHeaders(client);
 164
 0165            using var httpResponse = await client.GetAsync(requestUri);
 0166            var jsonResponse = await httpResponse.Content.ReadAsStringAsync();
 167
 0168            if (!httpResponse.IsSuccessStatusCode)
 169            {
 0170                await HandleErrorAsync($"HTTP ошибка {httpResponse.StatusCode} при запросе к {_clientName}",
 0171                    new HttpRequestException($"Status: {httpResponse.StatusCode}, Response: {jsonResponse}"),
 0172                    requestUri);
 0173                return string.Empty;
 174            }
 175
 0176            return jsonResponse;
 177        }
 178        catch (HttpRequestException ex)
 179        {
 0180            await HandleErrorAsync($"HTTP ошибка при запросе к {_clientName}", ex, requestUri);
 0181            return string.Empty;
 182        }
 183        catch (TaskCanceledException ex)
 184        {
 0185            await HandleErrorAsync($"Таймаут при запросе к {_clientName}", ex, requestUri);
 0186            return string.Empty;
 187        }
 188        catch (System.Net.Sockets.SocketException ex)
 189        {
 0190            await HandleErrorAsync($"Ошибка соединения с {_clientName}", ex, requestUri);
 0191            return string.Empty;
 192        }
 193        catch (Exception ex)
 194        {
 0195            await HandleErrorAsync($"Неожиданная ошибка при запросе к {_clientName}", ex, requestUri);
 0196            return string.Empty;
 197        }
 0198    }
 199
 200    /// <summary>
 201    /// Устанавливает заголовки для HttpWebRequest (переопределяется в наследниках)
 202    /// </summary>
 203    protected virtual void SetRequestHeaders(HttpWebRequest request)
 204    {
 205        // Переопределяется в наследниках
 0206    }
 207
 208    /// <summary>
 209    /// Устанавливает заголовки для HttpClient (переопределяется в наследниках)
 210    /// </summary>
 211    protected virtual void SetHttpClientHeaders(HttpClient client)
 212    {
 213        // Переопределяется в наследниках
 0214    }
 215
 216    /// <summary>
 217    /// Обрабатывает ошибку синхронно и отправляет уведомление по email
 218    /// </summary>
 219    protected virtual JObject HandleError(string errorMessage, Exception ex, string requestUri)
 220    {
 0221        var fullMessage = $"{errorMessage}\nURI: {requestUri}\nException: {ex}";
 222
 223        // Отправляем уведомление асинхронно (fire and forget)
 0224        if (_emailService != null)
 225        {
 0226            _ = Task.Run(async () =>
 0227            {
 0228                await Task.CompletedTask;
 0229                try
 0230                {
 0231                   // await _emailService.SendEmailAsync($"Ошибка API {_clientName}", fullMessage);
 0232                   Console.WriteLine("Ошибка API {_clientName}", fullMessage);
 0233                }
 0234                catch
 0235                {
 0236                    // Игнорируем ошибки отправки email
 0237                }
 0238            });
 239        }
 240
 0241        return new JObject();
 242    }
 243
 244    /// <summary>
 245    /// Обрабатывает ошибку асинхронно и отправляет уведомление по email
 246    /// </summary>
 247    protected virtual async Task HandleErrorAsync(string errorMessage, Exception ex, string requestUri)
 248    {
 1249        var fullMessage = $"{errorMessage}\nURI: {requestUri}\nException: {ex}";
 1250        Console.WriteLine($"[{_clientName}] {fullMessage}");
 251
 1252        if (_emailService != null)
 253        {
 254            try
 255            {
 1256                await _emailService.SendEmailAsync($"Ошибка API {_clientName}", fullMessage);
 1257            }
 0258            catch
 259            {
 260                // Игнорируем ошибки отправки email
 0261            }
 262        }
 1263    }
 264
 265    /// <summary>
 266    /// Абстрактный метод для тестового запроса (должен быть реализован в наследниках)
 267    /// </summary>
 268    public abstract Task<bool> GetTestRequest(string businessId, string apiKey);
 269
 270    /// <summary>
 271    /// Создает Polly политику для повторных попыток
 272    /// </summary>
 273    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
 274    {
 0275        return HttpPolicyExtensions
 0276            .HandleTransientHttpError()
 0277            .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
 0278            .WaitAndRetryAsync(
 0279                retryCount: 3,
 0280                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
 0281                onRetry: (outcome, timespan, retryCount, context) =>
 0282                {
 0283                    // Логирование можно добавить здесь
 0284                });
 285    }
 286
 287    /// <summary>
 288    /// Политика повторов Partner API Яндекс.Маркета.
 289    /// </summary>
 290    /// <remarks>
 291    /// Маркет сигнализирует о превышении лимита параллельных запросов кодом 420,
 292    /// а не стандартным 429. Такой ответ кратковременный и должен повторяться.
 293    /// </remarks>
 294    public static IAsyncPolicy<HttpResponseMessage> GetYandexRetryPolicy()
 295    {
 1296        return HttpPolicyExtensions
 1297            .HandleTransientHttpError()
 2298            .OrResult(msg => (int)msg.StatusCode == 420)
 1299            .WaitAndRetryAsync(
 1300                retryCount: 3,
 2301                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
 302    }
 303
 304    /// <summary>
 305    /// Политика повторов для запросов, ответ которых ждёт пользователь.
 306    /// </summary>
 307    /// <remarks>
 308    /// <see cref="GetRetryPolicy"/> ждёт между попытками 2, 4 и 8 секунд — для фоновой
 309    /// интеграции это нормально, а для страницы означает, что упавший сервис держит
 310    /// запрос четырнадцать секунд и всё равно отдаёт ошибку. Здесь одна быстрая
 311    /// повторная попытка: она снимает случайный 502 и не растягивает ожидание.
 312    /// </remarks>
 313    public static IAsyncPolicy<HttpResponseMessage> GetInteractiveRetryPolicy()
 314    {
 0315        return HttpPolicyExtensions
 0316            .HandleTransientHttpError()
 0317            .WaitAndRetryAsync(retryCount: 1, sleepDurationProvider: _ => TimeSpan.FromMilliseconds(500));
 318    }
 319
 320    /// <summary>
 321    /// Создает Polly политику для Circuit Breaker
 322    /// </summary>
 323    public static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
 324    {
 0325        return HttpPolicyExtensions
 0326            .HandleTransientHttpError()
 0327            .CircuitBreakerAsync(
 0328                handledEventsAllowedBeforeBreaking: 3,
 0329                durationOfBreak: TimeSpan.FromMinutes(1));
 330    }
 331}