Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Use HttpClient wrappers that ensure success to match FusionCache expectations #684

Merged
merged 8 commits into from
Apr 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,7 @@ await SendRequest<List<AuthorizedPartiesResultDto>>(

private async Task<T?> SendRequest<T>(string url, object request, CancellationToken cancellationToken)
{
var requestJson = JsonSerializer.Serialize(request, SerializerOptions);
_logger.LogDebug("Authorization request to {Url}: {RequestJson}", url, requestJson);
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(url, httpContent, cancellationToken);
if (response.StatusCode != HttpStatusCode.OK)
{
var errorResponse = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogWarning("AltinnAuthorizationClient.SendRequest failed with non-successful status code: {StatusCode} {Response}",
response.StatusCode, errorResponse);

return default;
}

var responseData = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonSerializer.Deserialize<T>(responseData, SerializerOptions);
_logger.LogDebug("Authorization request to {Url}: {RequestJson}", url, JsonSerializer.Serialize(request, SerializerOptions));
return await _httpClient.PostAsJsonEnsuredAsync<T>(url, request, serializerOptions: SerializerOptions, cancellationToken: cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Infrastructure.Common.Serialization;
using Microsoft.Extensions.Logging;
Expand All @@ -16,16 +15,13 @@ public AltinnEventsClient(HttpClient client)
}

public async Task Publish(CloudEvent cloudEvent, CancellationToken cancellationToken)
{
var uriBuilder = new UriBuilder(_client.BaseAddress!) { Path = "/events/api/v1/events" };
var msg = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Uri)
{
Content = JsonContent.Create(cloudEvent, options: SerializerOptions.CloudEventSerializerOptions)
};
msg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/cloudevents+json");
var response = await _client.SendAsync(msg, cancellationToken);
response.EnsureSuccessStatusCode();
}
=> await _client.PostAsJsonEnsuredAsync(
"/events/api/v1/events",
cloudEvent,
serializerOptions: SerializerOptions.CloudEventSerializerOptions,
configureContentHeaders: h
=> h.ContentType = new MediaTypeHeaderValue("application/cloudevents+json"),
cancellationToken: cancellationToken);
}

internal class ConsoleLogEventBus : ICloudEventBus
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Digdir.Domain.Dialogporten.Application.Externals;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry;

internal class NameRegistryClient : INameRegistry
{
private static readonly DistributedCacheEntryOptions OneDayCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) };
private static readonly DistributedCacheEntryOptions ZeroCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.MinValue };

private readonly IFusionCache _cache;
private readonly HttpClient _client;
private readonly ILogger<NameRegistryClient> _logger;

private static readonly JsonSerializerOptions SerializerOptions = new()

{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};

public NameRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider, ILogger<NameRegistryClient> logger)
public NameRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_cache = cacheProvider.GetCache(nameof(NameRegistry)) ?? throw new ArgumentNullException(nameof(cacheProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<string?> GetName(string personalIdentificationNumber, CancellationToken cancellationToken)
Expand All @@ -51,24 +41,13 @@ public NameRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider,
]
};

var requestJson = JsonSerializer.Serialize(nameLookup, SerializerOptions);
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");

var response = await _client.PostAsync(apiUrl, httpContent, cancellationToken);

if (response.StatusCode != HttpStatusCode.OK)
{
var errorResponse = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogWarning(
nameof(NameRegistryClient) + ".SendRequest failed with non-successful status code: {StatusCode} {Response}",
response.StatusCode, errorResponse);

return null;
}
var nameLookupResult = await _client.PostAsJsonEnsuredAsync<NameLookupResult>(
apiUrl,
nameLookup,
serializerOptions: SerializerOptions,
cancellationToken: cancellationToken);

var responseData = await response.Content.ReadAsStringAsync(cancellationToken);
var nameLookupResult = JsonSerializer.Deserialize<NameLookupResult>(responseData, SerializerOptions);
return nameLookupResult?.PartyNames.FirstOrDefault()?.Name;
return nameLookupResult.PartyNames.FirstOrDefault()?.Name;
}

private sealed class NameLookup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Diagnostics;
using System.Net.Http.Json;
using Digdir.Domain.Dialogporten.Application.Externals;
using Microsoft.Extensions.Caching.Distributed;
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;
Expand All @@ -10,9 +7,6 @@ internal class OrganizationRegistryClient : IOrganizationRegistry
{
private const string OrgNameReferenceCacheKey = "OrgNameReference";

private static readonly DistributedCacheEntryOptions OneDayCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) };
private static readonly DistributedCacheEntryOptions ZeroCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.MinValue };

private readonly IFusionCache _cache;
private readonly HttpClient _client;

Expand All @@ -33,8 +27,9 @@ public OrganizationRegistryClient(HttpClient client, IFusionCacheProvider cacheP
private async Task<Dictionary<string, OrganizationInfo>> GetOrgInfo(CancellationToken cancellationToken)
{
const string searchEndpoint = "orgs/altinn-orgs.json";

var response = await _client
.GetFromJsonAsync<OrganizationRegistryResponse>(searchEndpoint, cancellationToken) ?? throw new UnreachableException();
.GetFromJsonEnsuredAsync<OrganizationRegistryResponse>(searchEndpoint, cancellationToken: cancellationToken);

var orgInfoByOrgNumber = response
.Orgs
Expand Down Expand Up @@ -65,4 +60,4 @@ private sealed class OrganizationDetails
public string? Homepage { get; init; }
public IList<string>? Environments { get; init; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ public async Task<IReadOnlyCollection<string>> GetResourceIds(string org, Cancel
private async Task<Dictionary<string, string[]>> GetResourceIdsByOrg(CancellationToken cancellationToken)
{
const string searchEndpoint = "resourceregistry/api/v1/resource/resourcelist";

var response = await _client
.GetFromJsonAsync<List<ResourceRegistryResponse>>(searchEndpoint, cancellationToken)
?? throw new UnreachableException();
.GetFromJsonEnsuredAsync<List<ResourceRegistryResponse>>(searchEndpoint,
cancellationToken: cancellationToken);

var resourceIdsByOrg = response
.Where(x => x.ResourceType is ResourceTypeGenericAccess or ResourceTypeAltinnApp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

namespace Digdir.Domain.Dialogporten.Infrastructure;

public static class HttpClientExtensions
{
public static async Task<T> GetFromJsonEnsuredAsync<T>(
this HttpClient client,
string requestUri,
Action<HttpRequestHeaders>? configureHeaders = null,
CancellationToken cancellationToken = default)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
configureHeaders?.Invoke(httpRequestMessage.Headers);
var response = await client.SendAsync(httpRequestMessage, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellationToken);
return result is null
? throw new InvalidOperationException($"Failed to deserialize JSON from {requestUri}")
: result;
}

public static async Task<HttpResponseMessage> PostAsJsonEnsuredAsync(
this HttpClient client,
string requestUri,
object content,
Action<HttpRequestHeaders>? configureHeaders = null,
Action<HttpContentHeaders>? configureContentHeaders = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)
{
Content = JsonContent.Create(content, options: serializerOptions)
};
configureHeaders?.Invoke(httpRequestMessage.Headers);
configureContentHeaders?.Invoke(httpRequestMessage.Content.Headers);
var response = await client.SendAsync(httpRequestMessage, cancellationToken);
response.EnsureSuccessStatusCode();
return response;
}

public static async Task<T> PostAsJsonEnsuredAsync<T>(
this HttpClient client,
string requestUri,
object content,
Action<HttpRequestHeaders>? configureHeaders = null,
Action<HttpContentHeaders>? configureContentHeaders = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
var response = await client.PostAsJsonEnsuredAsync(requestUri, content, configureHeaders,
configureContentHeaders, serializerOptions, cancellationToken);
var result = await response.Content.ReadFromJsonAsync<T>(cancellationToken: cancellationToken);
return result is null
? throw new InvalidOperationException($"Failed to deserialize JSON to type {typeof(T).FullName} from {requestUri}")
: result;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using Altinn.Authorization.ABAC.Constants;
elsand marked this conversation as resolved.
Show resolved Hide resolved

namespace Digdir.Domain.Dialogporten.WebApi.Common.Extensions;

Expand Down Expand Up @@ -64,6 +65,15 @@ public static object ResponseBuilder(List<ValidationFailure> failures, HttpConte
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
},
StatusCodes.Status502BadGateway => new ProblemDetails
{
Title = "Bad gateway.",
Detail = "An upstream server returned an invalid response. Please try again later.",
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.3",
Status = statusCode,
Instance = ctx.Request.Path,
Extensions = { { "traceId", Activity.Current?.Id ?? ctx.TraceIdentifier } }
},
_ => new ProblemDetails
{
Title = "An error occurred while processing the request.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public static IApplicationBuilder UseProblemDetailsExceptionHandler(this IApplic
var error = exHandlerFeature.Error.Message;
var logger = ctx.Resolve<ILogger<ExceptionHandler>>();
logger.LogError(exHandlerFeature.Error, "{@Http}{@Type}{@Reason}", http, type, error);
ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
ctx.Response.StatusCode = type == nameof(HttpRequestException)
elsand marked this conversation as resolved.
Show resolved Hide resolved
? StatusCodes.Status502BadGateway
: StatusCodes.Status500InternalServerError;
ctx.Response.ContentType = "application/problem+json";
await ctx.Response.WriteAsJsonAsync(ctx.ResponseBuilder(ctx.Response.StatusCode));
});
Expand Down