diff --git a/src/HttpClientFactory/Polly/src/PolicyHttpMessageHandler.cs b/src/HttpClientFactory/Polly/src/PolicyHttpMessageHandler.cs index f2de2588d944..8f973a98db21 100644 --- a/src/HttpClientFactory/Polly/src/PolicyHttpMessageHandler.cs +++ b/src/HttpClientFactory/Polly/src/PolicyHttpMessageHandler.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -73,6 +73,7 @@ namespace Microsoft.Extensions.Http /// public class PolicyHttpMessageHandler : DelegatingHandler { + private const string PriorResponseKey = "PolicyHttpMessageHandler.PriorResponse"; private readonly IAsyncPolicy _policy; private readonly Func> _policySelector; @@ -147,7 +148,7 @@ protected override async Task SendAsync(HttpRequestMessage /// The . /// The . /// Returns a that will yield a response when completed. - protected virtual Task SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken) + protected virtual async Task SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken) { if (request == null) { @@ -159,7 +160,18 @@ protected virtual Task SendCoreAsync(HttpRequestMessage req throw new ArgumentNullException(nameof(context)); } - return base.SendAsync(request, cancellationToken); + if (request.Properties.TryGetValue(PriorResponseKey, out var priorResult) && priorResult is IDisposable disposable) + { + // This is a retry, dispose the prior response to free up the connection. + request.Properties.Remove(PriorResponseKey); + disposable.Dispose(); + } + + var result = await base.SendAsync(request, cancellationToken); + + request.Properties.Add(PriorResponseKey, result); + + return result; } private IAsyncPolicy SelectPolicy(HttpRequestMessage request) diff --git a/src/HttpClientFactory/Polly/test/PolicyHttpMessageHandlerTest.cs b/src/HttpClientFactory/Polly/test/PolicyHttpMessageHandlerTest.cs index c6de301cf913..98965d119d54 100644 --- a/src/HttpClientFactory/Polly/test/PolicyHttpMessageHandlerTest.cs +++ b/src/HttpClientFactory/Polly/test/PolicyHttpMessageHandlerTest.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Internal; using Polly; +using Polly.Extensions.Http; using Polly.Timeout; using Xunit; @@ -98,6 +99,54 @@ public async Task SendAsync_DynamicPolicy_PolicyTriggers_CanReexecuteSendAsync() Assert.Same(expectedRequest, policySelectorRequest); } + [Fact] + public async Task SendAsync_StaticPolicy_PolicyTriggers_CanReexecuteSendAsync_FirstResponseDisposed() + { + // Arrange + var policy = HttpPolicyExtensions.HandleTransientHttpError() + .RetryAsync(retryCount: 1); + + var callCount = 0; + var fakeContent = new FakeContent(); + var firstResponse = new HttpResponseMessage() + { + StatusCode = System.Net.HttpStatusCode.InternalServerError, + Content = fakeContent, + }; + var expected = new HttpResponseMessage(); + + var handler = new PolicyHttpMessageHandler(policy); + handler.InnerHandler = new TestHandler() + { + OnSendAsync = (req, ct) => + { + if (callCount == 0) + { + callCount++; + return Task.FromResult(firstResponse); + } + else if (callCount == 1) + { + callCount++; + return Task.FromResult(expected); + } + else + { + throw new InvalidOperationException(); + } + } + }; + var invoke = new HttpMessageInvoker(handler); + + // Act + var response = await invoke.SendAsync(new HttpRequestMessage(), CancellationToken.None); + + // Assert + Assert.Equal(2, callCount); + Assert.Same(expected, response); + Assert.True(fakeContent.Disposed); + } + [Fact] public async Task SendAsync_DynamicPolicy_PolicySelectorReturnsNull_ThrowsException() { @@ -333,5 +382,31 @@ protected override Task SendCoreAsync(HttpRequestMessage re return OnSendAsync(request, context, cancellationToken); } } + + private class TestHandler : HttpMessageHandler + { + public Func> OnSendAsync { get; set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Assert.NotNull(OnSendAsync); + return OnSendAsync(request, cancellationToken); + } + } + + private class FakeContent : StringContent + { + public FakeContent() : base("hello world") + { + } + + public bool Disposed { get; set; } + + protected override void Dispose(bool disposing) + { + Disposed = true; + base.Dispose(disposing); + } + } } }