File: PolicyHttpMessageHandlerTest.cs
Web Access
Project: src\src\HttpClientFactory\Polly\test\Microsoft.Extensions.Http.Polly.Tests.csproj (Microsoft.Extensions.Http.Polly.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
using Polly;
using Polly.Extensions.Http;
using Polly.Timeout;
using Xunit;
 
namespace Microsoft.Extensions.Http;
 
public class PolicyHttpMessageHandlerTest
{
    [Fact]
    public async Task SendAsync_StaticPolicy_PolicyTriggers_CanReexecuteSendAsync()
    {
        // Arrange
        var policy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .RetryAsync(retryCount: 5);
 
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        var callCount = 0;
        var expected = new HttpResponseMessage();
        handler.OnSendAsync = (req, c, ct) =>
        {
            if (callCount == 0)
            {
                callCount++;
                throw new HttpRequestException();
            }
            else if (callCount == 1)
            {
                callCount++;
                return Task.FromResult(expected);
            }
            else
            {
                throw new InvalidOperationException();
            }
        };
 
        // Act
        var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
 
        // Assert
        Assert.Equal(2, callCount);
        Assert.Same(expected, response);
    }
 
    [Fact]
    public async Task SendAsync_DynamicPolicy_PolicyTriggers_CanReexecuteSendAsync()
    {
        // Arrange
        var policy = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .RetryAsync(retryCount: 5);
 
        var expectedRequest = new HttpRequestMessage();
 
        HttpRequestMessage policySelectorRequest = null;
        var handler = new TestPolicyHttpMessageHandler((req) =>
        {
            policySelectorRequest = req;
            return policy;
        });
 
        var callCount = 0;
        var expected = new HttpResponseMessage();
        handler.OnSendAsync = (req, c, ct) =>
        {
            if (callCount == 0)
            {
                callCount++;
                throw new HttpRequestException();
            }
            else if (callCount == 1)
            {
                callCount++;
                return Task.FromResult(expected);
            }
            else
            {
                throw new InvalidOperationException();
            }
        };
 
        // Act
        var response = await handler.SendAsync(expectedRequest, CancellationToken.None);
 
        // Assert
        Assert.Equal(2, callCount);
        Assert.Same(expected, response);
        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 MultipleHandlers_CanReexecuteSendAsync_FirstResponseDisposed()
    {
        // Arrange
        var policy1 = HttpPolicyExtensions.HandleTransientHttpError()
            .RetryAsync(retryCount: 1);
        var policy2 = HttpPolicyExtensions.HandleTransientHttpError()
            .CircuitBreakerAsync(handledEventsAllowedBeforeBreaking: 2, durationOfBreak: TimeSpan.FromSeconds(10));
 
        var callCount = 0;
        var fakeContent = new FakeContent();
        var firstResponse = new HttpResponseMessage()
        {
            StatusCode = System.Net.HttpStatusCode.InternalServerError,
            Content = fakeContent,
        };
        var expected = new HttpResponseMessage();
 
        var handler1 = new PolicyHttpMessageHandler(policy1);
        var handler2 = new PolicyHttpMessageHandler(policy2);
        handler1.InnerHandler = handler2;
        handler2.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(handler1);
 
        // 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()
    {
        // Arrange
        var handler = new TestPolicyHttpMessageHandler((req) =>
        {
            return null;
        });
 
        var expected = new HttpResponseMessage();
 
        // Act
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        {
            await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
        });
 
        // Assert
        Assert.Equal(
            "The 'policySelector' function must return a non-null policy instance. To create a policy that takes no action, use 'Policy.NoOpAsync<HttpResponseMessage>()'.",
            exception.Message);
    }
 
    [Fact]
    public async Task SendAsync_PolicyCancellation_CanTriggerRequestCancellation()
    {
        // Arrange
        var policy = Policy<HttpResponseMessage>
            .Handle<TimeoutRejectedException>() // Handle timeouts by retrying
            .RetryAsync(retryCount: 5)
            .WrapAsync(Policy
                .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromMilliseconds(50)) // Apply a 50ms timeout
                .WrapAsync(Policy.NoOpAsync<HttpResponseMessage>()));
 
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        var @event = new ManualResetEventSlim(initialState: false);
 
        var callCount = 0;
        var expected = new HttpResponseMessage();
        handler.OnSendAsync = (req, c, ct) =>
        {
            // The inner cancellation token is created by Polly, it will trigger the timeout.
            Assert.True(ct.CanBeCanceled);
            if (callCount == 0)
            {
                callCount++;
                @event.Wait(ct);
                throw null; // unreachable, previous line should throw
            }
            else if (callCount == 1)
            {
                callCount++;
                return Task.FromResult(expected);
            }
            else
            {
                throw new InvalidOperationException();
            }
        };
 
        // Act
        var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None);
 
        // Assert
        Assert.Equal(2, callCount);
        Assert.Same(expected, response);
    }
 
    [Fact]
    public async Task SendAsync_NoContextSet_CreatesNewContext()
    {
        // Arrange
        var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        Context context = null;
        var expected = new HttpResponseMessage();
        handler.OnSendAsync = (req, c, ct) =>
        {
            context = c;
            Assert.NotNull(context);
            Assert.Same(context, req.GetPolicyExecutionContext());
            return Task.FromResult(expected);
        };
 
        var request = new HttpRequestMessage();
 
        // Act
        var response = await handler.SendAsync(request, CancellationToken.None);
 
        // Assert
        Assert.NotNull(context);
        Assert.Null(request.GetPolicyExecutionContext()); // We clean up the context if it was generated by the handler rather than caller supplied.
        Assert.Same(expected, response);
    }
 
    [Fact]
    public async Task SendAsync_ExistingContext_ReusesContext()
    {
        // Arrange
        var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        var expected = new HttpResponseMessage();
        var expectedContext = new Context(Guid.NewGuid().ToString());
 
        Context context = null;
        handler.OnSendAsync = (req, c, ct) =>
        {
            context = c;
            Assert.NotNull(c);
            Assert.Same(c, req.GetPolicyExecutionContext());
            return Task.FromResult(expected);
        };
 
        var request = new HttpRequestMessage();
        request.SetPolicyExecutionContext(expectedContext);
 
        // Act
        var response = await handler.SendAsync(request, CancellationToken.None);
 
        // Assert
        Assert.Same(expectedContext, context);
        Assert.Same(expectedContext, request.GetPolicyExecutionContext()); // We don't clean up the context if the caller or earlier delegating handlers had supplied it.
        Assert.Same(expected, response);
    }
 
    [Fact]
    public async Task SendAsync_NoContextSet_DynamicPolicySelectorThrows_CleansUpContext()
    {
        // Arrange
        var handler = new TestPolicyHttpMessageHandler((req) =>
        {
            throw new InvalidOperationException();
        });
 
        var request = new HttpRequestMessage();
 
        // Act
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        {
            await handler.SendAsync(request, CancellationToken.None);
        });
 
        // Assert
        Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the policy selector throws.
    }
 
    [Fact]
    public async Task SendAsync_NoContextSet_RequestThrows_CleansUpContext()
    {
        // Arrange
        var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        Context context = null;
        handler.OnSendAsync = (req, c, ct) =>
        {
            context = c;
            throw new OperationCanceledException();
        };
 
        var request = new HttpRequestMessage();
 
        // Act
        var exception = await Assert.ThrowsAsync<OperationCanceledException>(async () =>
        {
            await handler.SendAsync(request, CancellationToken.None);
        });
 
        // Assert
        Assert.NotNull(context); // The handler did generate a context for the execution.
        Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the execution throws.
    }
 
    [Fact]
    public void SendAsync_WorksInSingleThreadedSyncContext()
    {
        // Arrange
        var policy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));
        var handler = new TestPolicyHttpMessageHandler(policy);
 
        handler.OnSendAsync = async (req, c, ct) =>
        {
            await Task.Delay(1).ConfigureAwait(false);
            return null;
        };
 
        var hangs = true;
 
        // Act
        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
        {
            var token = cts.Token;
            token.Register(() => throw new OperationCanceledException(token));
 
            SingleThreadedSynchronizationContext.Run(() =>
            {
                // Act
                var request = new HttpRequestMessage();
                handler.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult();
                hangs = false;
            });
        }
 
        // Assert
        Assert.False(hangs);
    }
 
    private class TestPolicyHttpMessageHandler : PolicyHttpMessageHandler
    {
        public Func<HttpRequestMessage, Context, CancellationToken, Task<HttpResponseMessage>> OnSendAsync { get; set; }
 
        public TestPolicyHttpMessageHandler(IAsyncPolicy<HttpResponseMessage> policy)
            : base(policy)
        {
        }
 
        public TestPolicyHttpMessageHandler(Func<HttpRequestMessage, IAsyncPolicy<HttpResponseMessage>> policySelector)
            : base(policySelector)
        {
        }
 
        public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return base.SendAsync(request, cancellationToken);
        }
 
        protected override Task<HttpResponseMessage> SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken)
        {
            Assert.NotNull(OnSendAsync);
            return OnSendAsync(request, context, cancellationToken);
        }
    }
 
    private class TestHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> OnSendAsync { get; set; }
 
        protected override Task<HttpResponseMessage> 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);
        }
    }
}