'Why is .NetCore HttpClient disposed in second retry in my Unit Test?

I want to test my httpclient post retry function in my UT, here I mock the HttpFactory:

both the first and second time, the HttpFactory always returns HttpStatusCode.InternalServerError

public class MyServiceClient
{
    private readonly IHttpClientFactory _clientFactory;

    public MyServiceClient(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<string> GetResponse(string test= "te")
    {
        using var client = _clientFactory.CreateClient("MyClient");
        var content = new StringContent("{}", Encoding.UTF8, "application/json");
        var response = await client.PostAsync("http://www.contoso.com/",content);
        if (!response.IsSuccessStatusCode)
        {
            throw new ApplicationException("Application Error!");
        }
        var result = await response.Content.ReadAsStringAsync();
        return result;
    }

    public async Task<string> PollyExecute()
    {
        try
        {
            var policy = Policy
                .Handle<Exception>()
                .WaitAndRetryAsync(3,
                    count => TimeSpan.FromSeconds(2),
                    (ex, timeSpan,retrycount, context) =>
                    {
                        Console.WriteLine(ex);
                    });

            var response = await policy.ExecuteAsync(()=>GetResponse());

            return response;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw ;
        }

    }

}

Then I use my policy to run the client postasync method, there is no issue in my first retry, I get the excepted 500 internal server error.

public class HttpClientTest
{
    [Fact]
    public async Task PoliceTest()
    {
        var messageHandler = new StubHttpMessageHandler(HttpStatusCode.InternalServerError, "Error!!!!");

        var httpClient = new HttpClient(messageHandler)
        {
            BaseAddress = new Uri("http://mockuri")
        };

        var factory = Substitute.For<IHttpClientFactory>();
        factory.CreateClient(Arg.Any<string>()).Returns(httpClient, httpClient);

        var client = new MyServiceClient(factory);

        var result = await client.PollyExecute();

    }
}

public sealed class StubHttpMessageHandler : HttpMessageHandler
{
    public string _response;
    public HttpStatusCode _statusCode;

    public StubHttpMessageHandler(HttpStatusCode statusCode, string response)
    {
        _statusCode = statusCode;
        _response = response;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return Task.FromResult(Send(request));
    }

    private HttpResponseMessage Send(HttpRequestMessage request)
    {
        return new HttpResponseMessage
        {
            Content = new StringContent(_response),
            StatusCode = _statusCode,
        };
    }
}

However, in the second retry, when running postasync method,

It throws an exception says the httpclient is disposed. enter image description here enter image description here

Why? do some friends know the reason? thanks in advance!



Solution 1:[1]

Your mock returns the same httpClient object every time. HttpClient once disposed cannot be reused.

Setup mock to return a new instance every time:

    var factory = Substitute.For<IHttpClientFactory>();
    factory.CreateClient(Arg.Any<string>()).Returns( _ => new HttpClient(messageHandler)
           { 
              BaseAddress = new Uri("http://mockuri")
           });

Solution 2:[2]

I added more sample code, and I get a very first investigation:

At the Polly's second time, if I remove the Using in

using var client = _clientFactory.CreateClient("MyClient");

then there is no exception. I guess it should be caused by the client's scope, but actually when executing the PostAsync() , the Client still has some values, seems it's not disposed.

It's weird.

enter image description here enter image description here

Solution 3:[3]

Firstly, you need to setup in mock a new instance of HttpClient every time.

var factory = Substitute.For<IHttpClientFactory>();
            
factory.CreateClient(Arg.Any<string>())
    .Returns(new HttpClient(messageHandler)
    {
        BaseAddress = new Uri("http://mockuri")
    }, new HttpClient(messageHandler)
    {
        BaseAddress = new Uri("http://mockuri")
    });

Secondly, if you want to get no value without using clause, you need to call Dispose method manually after usage.

var client = _clientFactory.CreateClient("MyClient");
try
{        
    var content = new StringContent("{}", Encoding.UTF8, "application/json");
    var response = await client.PostAsync("http://www.contoso.com/", content);
    if (!response.IsSuccessStatusCode)
    //... your whole logic
}
finally
{
    if (client != null) { client.Dispose(); }
}

Solution 4:[4]

I saw a lot of posts and finally used setupsequence instead of setup and passed new httpclient everytime

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 YK1
Solution 2
Solution 3 marc_s
Solution 4 Rishabh Dugar