/src/HttpClientFactory/Polly/test/PolicyHttpMessageHandlerTest.cs

https://github.com/aspnet/Extensions · C# · 338 lines · 253 code · 53 blank · 32 comment · 15 complexity · 3cd54890eee64758a3bccc54a05b1bf5 MD5 · raw file

  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System;
  5. using System.Net.Http;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Microsoft.Extensions.Internal;
  9. using Polly;
  10. using Polly.Timeout;
  11. using Xunit;
  12. namespace Microsoft.Extensions.Http
  13. {
  14. public class PolicyHttpMessageHandlerTest
  15. {
  16. [Fact]
  17. public async Task SendAsync_StaticPolicy_PolicyTriggers_CanReexecuteSendAsync()
  18. {
  19. // Arrange
  20. var policy = Policy<HttpResponseMessage>
  21. .Handle<HttpRequestException>()
  22. .RetryAsync(retryCount: 5);
  23. var handler = new TestPolicyHttpMessageHandler(policy);
  24. var callCount = 0;
  25. var expected = new HttpResponseMessage();
  26. handler.OnSendAsync = (req, c, ct) =>
  27. {
  28. if (callCount == 0)
  29. {
  30. callCount++;
  31. throw new HttpRequestException();
  32. }
  33. else if (callCount == 1)
  34. {
  35. callCount++;
  36. return Task.FromResult(expected);
  37. }
  38. else
  39. {
  40. throw new InvalidOperationException();
  41. }
  42. };
  43. // Act
  44. var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
  45. // Assert
  46. Assert.Equal(2, callCount);
  47. Assert.Same(expected, response);
  48. }
  49. [Fact]
  50. public async Task SendAsync_DynamicPolicy_PolicyTriggers_CanReexecuteSendAsync()
  51. {
  52. // Arrange
  53. var policy = Policy<HttpResponseMessage>
  54. .Handle<HttpRequestException>()
  55. .RetryAsync(retryCount: 5);
  56. var expectedRequest = new HttpRequestMessage();
  57. HttpRequestMessage policySelectorRequest = null;
  58. var handler = new TestPolicyHttpMessageHandler((req) =>
  59. {
  60. policySelectorRequest = req;
  61. return policy;
  62. });
  63. var callCount = 0;
  64. var expected = new HttpResponseMessage();
  65. handler.OnSendAsync = (req, c, ct) =>
  66. {
  67. if (callCount == 0)
  68. {
  69. callCount++;
  70. throw new HttpRequestException();
  71. }
  72. else if (callCount == 1)
  73. {
  74. callCount++;
  75. return Task.FromResult(expected);
  76. }
  77. else
  78. {
  79. throw new InvalidOperationException();
  80. }
  81. };
  82. // Act
  83. var response = await handler.SendAsync(expectedRequest, CancellationToken.None);
  84. // Assert
  85. Assert.Equal(2, callCount);
  86. Assert.Same(expected, response);
  87. Assert.Same(expectedRequest, policySelectorRequest);
  88. }
  89. [Fact]
  90. public async Task SendAsync_DynamicPolicy_PolicySelectorReturnsNull_ThrowsException()
  91. {
  92. // Arrange
  93. var handler = new TestPolicyHttpMessageHandler((req) =>
  94. {
  95. return null;
  96. });
  97. var expected = new HttpResponseMessage();
  98. // Act
  99. var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
  100. {
  101. await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
  102. });
  103. // Assert
  104. Assert.Equal(
  105. "The 'policySelector' function must return a non-null policy instance. To create a policy that takes no action, use 'Policy.NoOpAsync<HttpResponseMessage>()'.",
  106. exception.Message);
  107. }
  108. [Fact]
  109. public async Task SendAsync_PolicyCancellation_CanTriggerRequestCancellation()
  110. {
  111. // Arrange
  112. var policy = Policy<HttpResponseMessage>
  113. .Handle<TimeoutRejectedException>() // Handle timeouts by retrying
  114. .RetryAsync(retryCount: 5)
  115. .WrapAsync(Policy
  116. .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromMilliseconds(50)) // Apply a 50ms timeout
  117. .WrapAsync(Policy.NoOpAsync<HttpResponseMessage>()));
  118. var handler = new TestPolicyHttpMessageHandler(policy);
  119. var @event = new ManualResetEventSlim(initialState: false);
  120. var callCount = 0;
  121. var expected = new HttpResponseMessage();
  122. handler.OnSendAsync = (req, c, ct) =>
  123. {
  124. // The inner cancellation token is created by Polly, it will trigger the timeout.
  125. Assert.True(ct.CanBeCanceled);
  126. if (callCount == 0)
  127. {
  128. callCount++;
  129. @event.Wait(ct);
  130. throw null; // unreachable, previous line should throw
  131. }
  132. else if (callCount == 1)
  133. {
  134. callCount++;
  135. return Task.FromResult(expected);
  136. }
  137. else
  138. {
  139. throw new InvalidOperationException();
  140. }
  141. };
  142. // Act
  143. var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
  144. // Assert
  145. Assert.Equal(2, callCount);
  146. Assert.Same(expected, response);
  147. }
  148. [Fact]
  149. public async Task SendAsync_NoContextSet_CreatesNewContext()
  150. {
  151. // Arrange
  152. var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
  153. var handler = new TestPolicyHttpMessageHandler(policy);
  154. Context context = null;
  155. var expected = new HttpResponseMessage();
  156. handler.OnSendAsync = (req, c, ct) =>
  157. {
  158. context = c;
  159. Assert.NotNull(context);
  160. Assert.Same(context, req.GetPolicyExecutionContext());
  161. return Task.FromResult(expected);
  162. };
  163. var request = new HttpRequestMessage();
  164. // Act
  165. var response = await handler.SendAsync(request, CancellationToken.None);
  166. // Assert
  167. Assert.NotNull(context);
  168. Assert.Null(request.GetPolicyExecutionContext()); // We clean up the context if it was generated by the handler rather than caller supplied.
  169. Assert.Same(expected, response);
  170. }
  171. [Fact]
  172. public async Task SendAsync_ExistingContext_ReusesContext()
  173. {
  174. // Arrange
  175. var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
  176. var handler = new TestPolicyHttpMessageHandler(policy);
  177. var expected = new HttpResponseMessage();
  178. var expectedContext = new Context(Guid.NewGuid().ToString());
  179. Context context = null;
  180. handler.OnSendAsync = (req, c, ct) =>
  181. {
  182. context = c;
  183. Assert.NotNull(c);
  184. Assert.Same(c, req.GetPolicyExecutionContext());
  185. return Task.FromResult(expected);
  186. };
  187. var request = new HttpRequestMessage();
  188. request.SetPolicyExecutionContext(expectedContext);
  189. // Act
  190. var response = await handler.SendAsync(request, CancellationToken.None);
  191. // Assert
  192. Assert.Same(expectedContext, context);
  193. Assert.Same(expectedContext, request.GetPolicyExecutionContext()); // We don't clean up the context if the caller or earlier delegating handlers had supplied it.
  194. Assert.Same(expected, response);
  195. }
  196. [Fact]
  197. public async Task SendAsync_NoContextSet_DynamicPolicySelectorThrows_CleansUpContext()
  198. {
  199. // Arrange
  200. var handler = new TestPolicyHttpMessageHandler((req) =>
  201. {
  202. throw new InvalidOperationException();
  203. });
  204. var request = new HttpRequestMessage();
  205. // Act
  206. var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
  207. {
  208. await handler.SendAsync(request, CancellationToken.None);
  209. });
  210. // Assert
  211. Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the policy selector throws.
  212. }
  213. [Fact]
  214. public async Task SendAsync_NoContextSet_RequestThrows_CleansUpContext()
  215. {
  216. // Arrange
  217. var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
  218. var handler = new TestPolicyHttpMessageHandler(policy);
  219. Context context = null;
  220. handler.OnSendAsync = (req, c, ct) =>
  221. {
  222. context = c;
  223. throw new OperationCanceledException();
  224. };
  225. var request = new HttpRequestMessage();
  226. // Act
  227. var exception = await Assert.ThrowsAsync<OperationCanceledException>(async () =>
  228. {
  229. await handler.SendAsync(request, CancellationToken.None);
  230. });
  231. // Assert
  232. Assert.NotNull(context); // The handler did generate a context for the execution.
  233. Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the execution throws.
  234. }
  235. [Fact]
  236. public void SendAsync_WorksInSingleThreadedSyncContext()
  237. {
  238. // Arrange
  239. var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
  240. var handler = new TestPolicyHttpMessageHandler(policy);
  241. handler.OnSendAsync = async (req, c, ct) =>
  242. {
  243. await Task.Delay(1).ConfigureAwait(false);
  244. return null;
  245. };
  246. var hangs = true;
  247. // Act
  248. using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
  249. {
  250. var token = cts.Token;
  251. token.Register(() => throw new OperationCanceledException(token));
  252. SingleThreadedSynchronizationContext.Run(() =>
  253. {
  254. // Act
  255. var request = new HttpRequestMessage();
  256. handler.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult();
  257. hangs = false;
  258. });
  259. }
  260. // Assert
  261. Assert.False(hangs);
  262. }
  263. private class TestPolicyHttpMessageHandler : PolicyHttpMessageHandler
  264. {
  265. public Func<HttpRequestMessage, Context, CancellationToken, Task<HttpResponseMessage>> OnSendAsync { get; set; }
  266. public TestPolicyHttpMessageHandler(IAsyncPolicy<HttpResponseMessage> policy)
  267. : base(policy)
  268. {
  269. }
  270. public TestPolicyHttpMessageHandler(Func<HttpRequestMessage, IAsyncPolicy<HttpResponseMessage>> policySelector)
  271. : base(policySelector)
  272. {
  273. }
  274. public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  275. {
  276. return base.SendAsync(request, cancellationToken);
  277. }
  278. protected override Task<HttpResponseMessage> SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken)
  279. {
  280. Assert.NotNull(OnSendAsync);
  281. return OnSendAsync(request, context, cancellationToken);
  282. }
  283. }
  284. }
  285. }