| | | 1 | | using System.Net; |
| | | 2 | | using Newtonsoft.Json; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.FidApi; |
| | | 5 | | |
| | | 6 | | /// <summary>Клиент фид-сервиса с ключом доступа.</summary> |
| | | 7 | | public sealed class FidApiHttpClient |
| | | 8 | | { |
| | | 9 | | private readonly IHttpClientFactory _factory; |
| | | 10 | | private readonly FidApiConfig _config; |
| | | 11 | | |
| | 45 | 12 | | public FidApiHttpClient(IHttpClientFactory factory, FidApiConfig config) |
| | | 13 | | { |
| | 45 | 14 | | _factory = factory; |
| | 45 | 15 | | _config = config; |
| | 45 | 16 | | } |
| | | 17 | | |
| | | 18 | | /// <summary>GET с моделью в теле ответа напрямую, без конверта. Возвращает null, если сервис ответил 404.</summary> |
| | | 19 | | public async Task<T?> GetWithoutEnvelopeAsync<T>(string path, IReadOnlyDictionary<string, string?>? query, |
| | | 20 | | CancellationToken ct = default) |
| | | 21 | | where T : class |
| | | 22 | | { |
| | 26 | 23 | | var (statusCode, text) = await SendAsync(path, query, ct); |
| | | 24 | | |
| | 23 | 25 | | if (statusCode == HttpStatusCode.NotFound) return null; |
| | | 26 | | |
| | 19 | 27 | | if (statusCode != HttpStatusCode.OK) |
| | 0 | 28 | | throw new FidApiException($"Фид-сервис вернул HTTP {(int)statusCode}.", (int)statusCode, path); |
| | | 29 | | |
| | | 30 | | try |
| | | 31 | | { |
| | 19 | 32 | | return JsonConvert.DeserializeObject<T>(text); |
| | | 33 | | } |
| | 0 | 34 | | catch (Exception ex) |
| | | 35 | | { |
| | 0 | 36 | | throw new FidApiException("Фид-сервис вернул невалидный JSON.", (int)statusCode, path, ex); |
| | | 37 | | } |
| | 21 | 38 | | } |
| | | 39 | | |
| | | 40 | | private async Task<(HttpStatusCode StatusCode, string Body)> SendAsync( |
| | | 41 | | string path, IReadOnlyDictionary<string, string?>? query, CancellationToken ct) |
| | | 42 | | { |
| | 26 | 43 | | if (string.IsNullOrWhiteSpace(_config.ApiKey)) |
| | 1 | 44 | | throw new FidApiException("Не настроен FidApi:ApiKey.", requestPath: path); |
| | | 45 | | |
| | 25 | 46 | | using var request = new HttpRequestMessage(HttpMethod.Get, BuildUri(path, query)); |
| | 25 | 47 | | request.Headers.Add("X-Api-Key", _config.ApiKey); |
| | | 48 | | |
| | 25 | 49 | | using var response = await _factory.CreateClient("FidApi").SendAsync(request, ct); |
| | 23 | 50 | | var text = await response.Content.ReadAsStringAsync(ct); |
| | | 51 | | |
| | 23 | 52 | | if (response.StatusCode == HttpStatusCode.Unauthorized) |
| | 1 | 53 | | throw new FidApiException("Фид-сервис отклонил ключ доступа (401). Проверьте FidApi:ApiKey.", |
| | 1 | 54 | | (int)response.StatusCode, path); |
| | | 55 | | |
| | | 56 | | // Для API переадресация означает неверный BaseUrl или прокси. |
| | 22 | 57 | | if (response.StatusCode is HttpStatusCode.Found or HttpStatusCode.Redirect) |
| | 1 | 58 | | throw new FidApiException($"Фид-сервис перенаправил запрос на форму входа: путь {path} не относится к API.", |
| | 1 | 59 | | (int)response.StatusCode, path); |
| | | 60 | | |
| | 21 | 61 | | return (response.StatusCode, text); |
| | 21 | 62 | | } |
| | | 63 | | |
| | | 64 | | private Uri BuildUri(string path, IReadOnlyDictionary<string, string?>? query) |
| | | 65 | | { |
| | 25 | 66 | | var builder = new UriBuilder(_config.BaseUrl.TrimEnd('/') + "/" + path.TrimStart('/')); |
| | 25 | 67 | | if (query != null) |
| | 22 | 68 | | builder.Query = string.Join("&", query |
| | 28 | 69 | | .Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
| | 46 | 70 | | .Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}")); |
| | 25 | 71 | | return builder.Uri; |
| | | 72 | | } |
| | | 73 | | } |