Files
jsmr/JSMR.Infrastructure/Http/ApiClient.cs
Brian Bicknell adfbf654a6
All checks were successful
ci / build-test (push) Successful in 2m30s
ci / publish-image (push) Successful in 2m1s
Added logic to remove supported languages that are no longer supported, rather than just being purely additive. Added ApiClient logging.
2026-03-30 23:03:53 -04:00

134 lines
5.1 KiB
C#

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<TResponse> GetJsonAsync<TResponse>(
string url,
Action<HttpRequestHeaders>? 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<TResponse>(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<TResponse> GetJsonpAsync<TResponse>(
string url,
Action<HttpRequestHeaders>? 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<TResponse>(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<TResponse> PostJsonAsync<TRequest, TResponse>(
string url,
TRequest payload,
Action<HttpRequestHeaders>? 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<TResponse>(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 = "<unable to read 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);
}
}