|
// 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);
}
}
}
|