Skip to content

Commit 80de6a5

Browse files
author
MIchael Yarichuk
committed
more code quality refactoring, ensure Polly message handler won't cause async deadlocks, more testing
1 parent b6e298c commit 80de6a5

5 files changed

Lines changed: 139 additions & 11 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Polly;
9+
using Polly.Timeout;
10+
using WireMock.RequestBuilders;
11+
using WireMock.ResponseBuilders;
12+
using WireMock.Server;
13+
using Xunit;
14+
15+
namespace Simple.HttpClientFactory.Tests
16+
{
17+
public class ExceptionTranslatorTests
18+
{
19+
private readonly WireMockServer _server;
20+
private readonly List<string> _visitedMiddleware = new List<string>();
21+
22+
public ExceptionTranslatorTests()
23+
{
24+
_server = WireMockServer.Start();
25+
_server.Given(Request.Create().WithPath("/hello/world").UsingAnyMethod())
26+
.RespondWith(
27+
Response.Create()
28+
.WithStatusCode(200)
29+
.WithHeader("Content-Type", "text/plain")
30+
.WithBody("Hello world!"));
31+
32+
_server
33+
.Given(Request.Create()
34+
.WithPath("/timeout")
35+
.UsingGet())
36+
.RespondWith(Response.Create()
37+
.WithStatusCode(408));
38+
}
39+
40+
public class TestException : Exception
41+
{
42+
public TestException(string message) : base(message)
43+
{
44+
}
45+
}
46+
47+
[Fact]
48+
public async Task Exception_translator_can_translate_exception_types()
49+
{
50+
var clientWithRetry = HttpClientFactory.Create()
51+
.WithMessageExceptionHandler(ex => true, ex => new TestException(ex.Message))
52+
.WithPolicy(
53+
Policy<HttpResponseMessage>
54+
.Handle<HttpRequestException>()
55+
.OrResult(result => (int)result.StatusCode >= 500 || result.StatusCode == HttpStatusCode.RequestTimeout)
56+
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(1)))
57+
.WithPolicy(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(4), TimeoutStrategy.Optimistic))
58+
.Build();
59+
60+
await Assert.ThrowsAsync<TestException>(() => clientWithRetry.GetAsync(_server.Urls[0] + "/timeout"));
61+
Assert.Equal(4, _server.LogEntries.Count());
62+
63+
}
64+
65+
66+
[Fact]
67+
public async Task Exception_translator_should_not_change_unhandled_exceptions()
68+
{
69+
var clientWithRetry = HttpClientFactory.Create()
70+
.WithMessageExceptionHandler(ex => true, ex => ex)
71+
.WithPolicy(
72+
Policy<HttpResponseMessage>
73+
.Handle<HttpRequestException>()
74+
.OrResult(result => (int)result.StatusCode >= 500 || result.StatusCode == HttpStatusCode.RequestTimeout)
75+
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(1)))
76+
.WithPolicy(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(4), TimeoutStrategy.Optimistic))
77+
.Build();
78+
79+
await Assert.ThrowsAsync<HttpRequestException>(() => clientWithRetry.GetAsync(_server.Urls[0] + "/timeout"));
80+
Assert.Equal(4, _server.LogEntries.Count());
81+
82+
}
83+
84+
[Fact]
85+
public async Task Exception_translator_without_errors_should_not_affect_anything()
86+
{
87+
var trafficRecorderMessageHandler = new TrafficRecorderMessageHandler(_visitedMiddleware);
88+
var eventMessageHandler = new EventMessageHandler(_visitedMiddleware);
89+
90+
var client = HttpClientFactory.Create()
91+
.WithMessageExceptionHandler(ex => true, ex => ex)
92+
.WithMessageHandler(eventMessageHandler)
93+
.WithMessageHandler(trafficRecorderMessageHandler)
94+
.Build();
95+
96+
var raisedEvent = await Assert.RaisesAsync<EventMessageHandler.RequestEventArgs>(
97+
h => eventMessageHandler.Request += h,
98+
h => eventMessageHandler.Request -= h,
99+
() => client.GetAsync(_server.Urls[0] + "/hello/world"));
100+
101+
Assert.True(raisedEvent.Arguments.Request.Headers.Contains("foobar"));
102+
Assert.Equal("foobar",raisedEvent.Arguments.Request.Headers.GetValues("foobar").FirstOrDefault());
103+
Assert.Single(trafficRecorderMessageHandler.Traffic);
104+
105+
Assert.Equal(HttpStatusCode.OK, trafficRecorderMessageHandler.Traffic[0].Item2.StatusCode);
106+
Assert.Equal(new [] { nameof(TrafficRecorderMessageHandler), nameof(EventMessageHandler) }, _visitedMiddleware);
107+
}
108+
109+
}
110+
}

Simple.HttpClientFactory.Tests/MiddlewareDelegateTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Net;
5+
using System.Net.Http;
56
using System.Threading.Tasks;
67
using WireMock.RequestBuilders;
78
using WireMock.ResponseBuilders;
@@ -26,6 +27,7 @@ public MiddlewareDelegateTests()
2627
.WithBody("Hello world!"));
2728
}
2829

30+
2931
[Fact]
3032
public async Task Single_middleware_handler_should_work()
3133
{

Simple.HttpClientFactory/HttpClientBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public IHttpClientBuilder WithTimeout(in TimeSpan timeout)
7171
public IHttpClientBuilder WithMessageExceptionHandler(
7272
Func<HttpRequestException, bool> exceptionHandlingPredicate,
7373
Func<HttpRequestException, Exception> exceptionHandler) =>
74-
WithMessageHandler(new ExceptionTranslatorRequestMiddleware(exceptionHandlingPredicate, exceptionHandler, null));
74+
WithMessageHandler(new ExceptionTranslatorRequestMiddleware(exceptionHandlingPredicate, exceptionHandler));
7575

7676
/// <exception cref="T:System.ArgumentNullException"><paramref name="handler"/> is <see langword="null"/></exception>
7777
public IHttpClientBuilder WithMessageHandler(DelegatingHandler handler)

Simple.HttpClientFactory/MessageHandlers/ExceptionTranslatorRequestMiddleware.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ public class ExceptionTranslatorRequestMiddleware : DelegatingHandler
1313
public event EventHandler<HttpRequestException> RequestException;
1414
public event EventHandler<Exception> TransformedRequestException;
1515

16+
public ExceptionTranslatorRequestMiddleware(
17+
Func<HttpRequestException, bool> exceptionHandlingPredicate,
18+
Func<HttpRequestException, Exception> exceptionHandler)
19+
{
20+
_exceptionHandlingPredicate = exceptionHandlingPredicate ?? throw new ArgumentNullException(nameof(exceptionHandlingPredicate));
21+
_exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler));
22+
}
23+
24+
1625
public ExceptionTranslatorRequestMiddleware(
1726
Func<HttpRequestException, bool> exceptionHandlingPredicate,
1827
Func<HttpRequestException, Exception> exceptionHandler, DelegatingHandler handler) : base(handler)

Simple.HttpClientFactory/MessageHandlers/PollyHttpMessageHandler.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,29 @@ private Task<HttpResponseMessage> SendAsyncInternal(HttpRequestMessage request,
4545
var cleanUpContext = false;
4646
var context = GetOrCreatePolicyExecutionContext(request, ref cleanUpContext);
4747

48-
return _policy.ExecuteAsync(
49-
async (c, ct) => await base.SendAsync(request, cancellationToken), context, cancellationToken)
50-
.ContinueWith(t =>
51-
{
52-
if(cleanUpContext)
53-
request.SetPolicyExecutionContext(null);
54-
return t.Result;
55-
}, cancellationToken);
48+
//do not await for the task so the async state machine won't grow big
49+
var responseTask =
50+
_policy.ExecuteAsync(
51+
async (c, ct) =>
52+
await base.SendAsync(request, cancellationToken), context, cancellationToken)
53+
.ContinueWith(t =>
54+
{
55+
if(cleanUpContext)
56+
request.SetPolicyExecutionContext(null);
57+
return t.Result;
58+
}, cancellationToken);
59+
60+
responseTask.ConfigureAwait(false);
61+
62+
return responseTask;
5663

57-
Context GetOrCreatePolicyExecutionContext(HttpRequestMessage httpRequestMessage, ref bool b)
64+
Context GetOrCreatePolicyExecutionContext(HttpRequestMessage httpRequestMessage, ref bool shouldCleanupContext)
5865
{
5966
if (!httpRequestMessage.TryGetPolicyExecutionContext(out var fetchedContext))
6067
{
6168
fetchedContext = new Context();
6269
httpRequestMessage.SetPolicyExecutionContext(fetchedContext);
63-
b = true;
70+
shouldCleanupContext = true;
6471
}
6572

6673
return fetchedContext;

0 commit comments

Comments
 (0)