Skip to content

Commit cc5246c

Browse files
authored
Feat/2428 503 on socket exception (#2429)
* feat: return 503 on socket exception when approving system user request
1 parent 85b410b commit cc5246c

3 files changed

Lines changed: 96 additions & 0 deletions

File tree

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Constants/Problem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ private static readonly ProblemDescriptorFactory _factory
185185
public static ProblemDescriptor DelegationRightResourceIsMaskinPortenSchema { get; }
186186
= _factory.Create(71, HttpStatusCode.Forbidden, "DelegationCheck failed with error: The resource is not delegable because it is a Maskinporten schema resource.");
187187

188+
/// <summary>
189+
/// Gets a <see cref="ProblemDescriptor"/>. Used when a call to a downstream API fails with a
190+
/// transient network error (socket exception), so that the caller may retry.
191+
/// </summary>
192+
public static ProblemDescriptor DownstreamApiUnavailable { get; }
193+
= _factory.Create(100, HttpStatusCode.ServiceUnavailable, "The downstream API could not be reached due to a transient network error. Please retry.");
194+
188195
/// <summary>
189196
/// Gets a <see cref="ProblemDescriptor"/>.
190197
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Integration/Clients/SystemUserRequestClient.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Net.Http.Json;
3+
using System.Net.Sockets;
34
using System.Text.Json;
45
using Altinn.AccessManagement.UI.Core.ClientInterfaces;
56
using Altinn.AccessManagement.UI.Core.Constants;
@@ -96,6 +97,11 @@ public async Task<Result<bool>> ApproveSystemUserRequest(int partyId, Guid reque
9697
_logger.LogError("AccessManagement.UI // SystemUserRequestClient // ApproveSystemUserRequest // Unexpected HttpStatusCode: {StatusCode}\n {responseBody}", response.StatusCode, responseContent);
9798
return ProblemMapper.MapToAuthUiError(responseContent, response.StatusCode);
9899
}
100+
catch (Exception ex) when (ex.GetBaseException() is SocketException)
101+
{
102+
_logger.LogError(ex, "AccessManagement.UI // SystemUserRequestClient // ApproveSystemUserRequest // Socket exception");
103+
return Problem.DownstreamApiUnavailable;
104+
}
99105
catch (Exception ex)
100106
{
101107
_logger.LogError(ex, "AccessManagement.UI // SystemUserRequestClient // ApproveSystemUserRequest // Exception");
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Net.Sockets;
4+
using Altinn.AccessManagement.UI.Integration.Clients;
5+
using Altinn.AccessManagement.UI.Integration.Configuration;
6+
using Altinn.Authorization.ProblemDetails;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using Microsoft.Extensions.Options;
10+
11+
namespace Altinn.AccessManagement.UI.Tests.Clients
12+
{
13+
/// <summary>
14+
/// Tests that socket errors towards the authentication API surface as a retryable
15+
/// 503 problem instead of bubbling up as an unhandled exception (500).
16+
/// </summary>
17+
public class SystemUserRequestClientTest
18+
{
19+
private const string BaseUrl = "http://localhost:5117/authentication/api/v1/";
20+
21+
private static readonly Guid _requestId = Guid.Parse("55555555-5555-5555-5555-555555555555");
22+
23+
/// <summary>
24+
/// Handler that fails every request with the given exception.
25+
/// </summary>
26+
private sealed class ThrowingHandler(Exception exception) : HttpMessageHandler
27+
{
28+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
29+
{
30+
throw exception;
31+
}
32+
}
33+
34+
private static SystemUserRequestClient CreateClient(Exception exception)
35+
{
36+
PlatformSettings platformSettings = new()
37+
{
38+
ApiAuthenticationEndpoint = BaseUrl,
39+
SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key",
40+
SubscriptionKey = "subscription-key",
41+
JwtCookieName = "AltinnStudioRuntime",
42+
};
43+
44+
return new SystemUserRequestClient(
45+
NullLogger<SystemUserRequestClient>.Instance,
46+
new HttpClient(new ThrowingHandler(exception)),
47+
new HttpContextAccessor { HttpContext = new DefaultHttpContext() },
48+
Options.Create(platformSettings));
49+
}
50+
51+
/// <summary>
52+
/// A socket error is transient, so callers must get a 503 they can retry on rather than a 500 —
53+
/// end users approving a request in the frontend, external clients calling the BFF, and e2e tests alike.
54+
/// </summary>
55+
[Fact]
56+
public async Task ApproveSystemUserRequest_SocketException_ReturnsServiceUnavailable()
57+
{
58+
// A dropped connection reaches the client as a SocketException nested inside HttpRequestException
59+
Exception socketError = new HttpRequestException(
60+
"An error occurred while sending the request.",
61+
new IOException("The response ended prematurely.", new SocketException((int)SocketError.ConnectionReset)));
62+
63+
Result<bool> result = await CreateClient(socketError).ApproveSystemUserRequest(51329012, _requestId, CancellationToken.None);
64+
65+
Assert.True(result.IsProblem);
66+
Assert.Equal(HttpStatusCode.ServiceUnavailable, (HttpStatusCode)result.Problem.StatusCode);
67+
Assert.Equal("AMUI-00100", result.Problem.ErrorCode.ToString());
68+
}
69+
70+
/// <summary>
71+
/// Only socket errors are transient. Every other failure must keep bubbling up as before, so
72+
/// genuine server-side errors are not disguised as something callers should retry.
73+
/// </summary>
74+
[Fact]
75+
public async Task ApproveSystemUserRequest_OtherException_Rethrows()
76+
{
77+
Exception otherError = new HttpRequestException("Bad gateway", new InvalidOperationException("nope"));
78+
79+
await Assert.ThrowsAsync<HttpRequestException>(
80+
() => CreateClient(otherError).ApproveSystemUserRequest(51329012, _requestId, CancellationToken.None));
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)