using Microsoft.Extensions.Logging; using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace JSMR.Infrastructure.Http; public abstract class ApiClient(HttpClient http, ILogger logger, JsonSerializerOptions? json = null) { protected async Task GetJsonAsync( string url, Action? configureHeaders = null, CancellationToken cancellationToken = default) { using HttpRequestMessage request = new(HttpMethod.Get, url); configureHeaders?.Invoke(request.Headers); LogRequest(request); using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await EnsureSuccess(response).ConfigureAwait(false); Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); try { return await JsonSerializer.DeserializeAsync(stream, json, cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}."); } catch (JsonException ex) { logger.LogError(ex, "Failed to deserialize JSON from {Url}. ContentLengthHeader={ContentLengthHeader}", url, response.Content.Headers.ContentLength); throw; } } protected async Task GetJsonpAsync( string url, Action? configureHeaders = null, CancellationToken cancellationToken = default) { using HttpRequestMessage request = new(HttpMethod.Get, url); configureHeaders?.Invoke(request.Headers); LogRequest(request); using HttpResponseMessage response = await http.SendAsync( request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await EnsureSuccess(response).ConfigureAwait(false); string body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); string jsonBody = ExtractJsonFromJsonp(body); return JsonSerializer.Deserialize(jsonBody, json) ?? throw new InvalidOperationException($"Failed to deserialize JSONP payload to {typeof(TResponse).Name} from {url}."); } private static string ExtractJsonFromJsonp(string body) { if (string.IsNullOrWhiteSpace(body)) throw new InvalidOperationException("Response body was empty."); body = body.Trim(); int firstParen = body.IndexOf('('); int lastParen = body.LastIndexOf(')'); if (firstParen < 0 || lastParen <= firstParen) throw new InvalidOperationException("Response was not valid JSONP."); return body[(firstParen + 1)..lastParen].Trim(); } protected async Task PostJsonAsync( string url, TRequest payload, Action? configureHeaders = null, CancellationToken cancellationToken = default) { StringContent content = new(JsonSerializer.Serialize(payload, json), Encoding.UTF8, "application/json"); using HttpRequestMessage request = new(HttpMethod.Post, url) { Content = content }; configureHeaders?.Invoke(request.Headers); LogRequest(request); using HttpResponseMessage response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); await EnsureSuccess(response).ConfigureAwait(false); Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync(stream, json, cancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}."); } protected virtual void LogRequest(HttpRequestMessage request) => logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri); protected virtual void LogFailure(HttpResponseMessage response, string body) => logger.LogWarning("HTTP {Status} for {Uri}. Body: {Body}", (int)response.StatusCode, response.RequestMessage?.RequestUri, Truncate(body, 500)); protected static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…"; protected async Task EnsureSuccess(HttpResponseMessage res) { if (res.IsSuccessStatusCode) return; string body; try { body = await res.Content.ReadAsStringAsync().ConfigureAwait(false); } catch { body = ""; } LogFailure(res, body); //Throw a richer exception(you can customize per API) throw new HttpRequestException( $"Request to {res.RequestMessage?.RequestUri} failed: {(int)res.StatusCode} {res.ReasonPhrase}. Body: {Truncate(body, 1000)}", null, res.StatusCode); } }