Search code examples
c#dotnet-httpclientmauipollyretry-logic

Polly - 'Cannot access a closed Stream'


I am upgrading a Xamarin app to MAUI and thought of decoupling things a bit. Before i had a datastore which handled all requests to an API, now i have a service for each section of the app from which requests go to a HttpManager, problem is when the policy retries, it works for the first time but on the second retry it fails with the message "Cannot access a closed Stream". Searched a bit but couldn't find a fix.

I call the service from the viewModel.

LoginViewModel.cs

readonly IAuthService _authService;

public LoginViewModel(IAuthService authService)
{
    _authService = authService;
}

[RelayCommand]
private async Task Login()
{
    ...
   
    var loginResponse = await _authService.Login(
        new LoginDTO(QRSettings.StaffCode, Password, QRSettings.Token));

    ...
}

In the service i set send the data to the HttpManager and process the response

AuthService.cs

private readonly IHttpManager _httpManager;

public AuthService(IHttpManager manager)
{
   _httpManager = manager;
}

public async Task<ServiceResponse<string>> Login(LoginDTO model)
{
    var json = JsonConvert.SerializeObject(model);
    var content = new StringContent(json, Encoding.UTF8, "application/json");
    var response = await _httpManager.PostAsync<string>("Auth/Login", content);

    ...
}

And in here i send the request.

HttpManager.cs

readonly IConnectivity _connectivity;

readonly AsyncPolicyWrap _retryPolicy = Policy
   .Handle<TimeoutRejectedException>()
   .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1), (exception, timespan, retryAttempt, context) =>
   {
       App.AppViewModel.RetryTextVisible = true;
       App.AppViewModel.RetryText = $"Attempt number {retryAttempt}...";
   })
   .WrapAsync(Policy.TimeoutAsync(11, TimeoutStrategy.Pessimistic));

HttpClient HttpClient;

public HttpManager(IConnectivity connectivity)
{
    _connectivity = connectivity;

    HttpClient = new HttpClient();
}

public async Task<ServiceResponse<T>> PostAsync<T>(string endpoint, HttpContent content, bool shouldRetry = true)
{
    ...

    // Post request
    var response = await Post($""http://10.0.2.2:5122/{endpoint}", content, shouldRetry);

    ...
}

async Task<HttpResponseMessage> Post(string url, HttpContent content, bool shouldRetry)
{
    if (shouldRetry)
    {
        // This is where the error occurs, in the PostAsync
        var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
            await HttpClient.PostAsync(url, content, token), CancellationToken.None);

        ...
    }

    ...
}

And this is the MauiProgram if it matters

...

private static MauiAppBuilder RegisterServices(this MauiAppBuilder builder)
{
    ...

    builder.Services.AddSingleton<IHttpManager, HttpManager>();
    builder.Services.AddSingleton<IAuthService, AuthService>();

    return builder;
}

Can't figure out what the issue is... I tried various try/catches, tried finding a solution online but no luck. On the second retry it always gives that error


Solution

  • Disclaimer: In the comments section I've suggested to rewind the underlying stream. That suggestion was wrong, let me correct myself.

    TL;DR: You can't reuse a HttpContent object you need to re-create it.


    In order to be able to perform a retry attempt with a POST verb you need to recreate the HttpContent payload for each attempt.

    There are several ways to fix your code:

    Pass the serialized string as parameter

    async Task<HttpResponseMessage> Post(string url, string content, bool shouldRetry)
    {
        if (shouldRetry)
        {
            var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
                await HttpClient.PostAsync(url, new StringContent(content, Encoding.UTF8, "application/json"), token), CancellationToken.None);
    
            ...
        }
    
        ...
    }
    

    Pass the to-be-serialized object as parameter

    async Task<HttpResponseMessage> Post(string url, object content, bool shouldRetry)
    {
        if (shouldRetry)
        {
            var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
                await HttpClient.PostAsync(url, JsonContent.Create(content), token), CancellationToken.None);
    
            ...
        }
    
        ...
    }
    
    • Here we are taking advantage of the JsonContent type which was introduced in .NET 5

    Pass the to-be-serialized object as parameter #2

    async Task<HttpResponseMessage> Post(string url, object content, bool shouldRetry)
    {
        if (shouldRetry)
        {
            var response = await _retryPolicy.ExecuteAndCaptureAsync(async token =>
                await HttpClient.PostAsJsonAsync(url, content, token), CancellationToken.None);
    
            ...
        }
    
        ...
    }