121 lines
4.8 KiB
C#
121 lines
4.8 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);
|
|
|
|
return await JsonSerializer.DeserializeAsync<TResponse>(stream, json, cancellationToken).ConfigureAwait(false)
|
|
?? throw new InvalidOperationException($"Failed to deserialize JSON to {typeof(TResponse).Name} from {url}.");
|
|
}
|
|
|
|
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);
|
|
}
|
|
} |