1

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

Peter Csala
  • 17,736
  • 16
  • 35
  • 75

1 Answers1

2

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

        ...
    }

    ...
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Coming back to this after a while, in the end i went with this `var response = await _retryPolicy.ExecuteAndCaptureAsync(async token => await HttpClient.PostAsync(url, new StringContent(json, Encoding.UTF8, "application/json"), token), CancellationToken.None);` and it has worked while using the local API on IIS but as soon as i connect to an API on a live server i get the `cannot access a closed stream error` again. Any ideas why ? :) – Adrian Radulescu Feb 06 '23 at 12:46
  • @AdrianRadulescu Could you please share with me the related code fragment via https://pastebin.com/? – Peter Csala Feb 06 '23 at 13:01
  • sure, https://pastebin.com/R3cERYqB, the other parts of the code are mainly the same, the only difference is that the class is static now, form which i control the requests sent from the app – Adrian Radulescu Feb 06 '23 at 13:24
  • @AdrianRadulescu How do you get the `HttpClient` instance? – Peter Csala Feb 06 '23 at 14:30
  • i just use the one that it's initialized at the start of the program. i also tried creating a new httpclient inside the **ExecuteAndCaptureAsync** of the polly policy but it still gives the same error. here is the whole class https://pastebin.com/NcVGMvZv – Adrian Radulescu Feb 06 '23 at 14:36
  • @AdrianRadulescu Could you please try the following two things: 1) Instead of having a `static` retry policy instance (`_retryPolicy`) can you please convert it to a method (`IAsyncPolicy GetRetryPolicy()`) ? 2) Can you please give it a try to directly inject the retry to the HttpClient (`new HttpClient(new PolicyHttpMessageHandler(GetRetryPolicy()) { InnerHandler = new HttpClientHandler()})`? In this case you don't need to explicitly wrap the `PostAsync` or `GetAsync` calls with the policy execute call. – Peter Csala Feb 07 '23 at 08:05
  • these are the modifications https://pastebin.com/fGNYGeVj, sadly the closed stream error still occurs – Adrian Radulescu Feb 07 '23 at 09:50
  • @AdrianRadulescu Why do you use pessimistic timeout? HttpClient does support cooperative cancellation. – Peter Csala Feb 07 '23 at 09:57
  • without it the timeout was all over the place from what i remember or it didn't work at all, this is the only way it did :) Even with optimistic timeout it does the same – Adrian Radulescu Feb 07 '23 at 10:06
  • @AdrianRadulescu Okay, so could you please provide a minimal reproducible example because there are too many moving parts in your provided class? – Peter Csala Feb 07 '23 at 10:15
  • https://github.com/radulescuadrian/MAUI-HttpService i made a simple repo where i can see the same problem appearing, on a live server i get that error but on a local one everything is ok, also the GET method returns fine on both servers, only the POST throws the closed steam error only on the live server – Adrian Radulescu Feb 07 '23 at 11:36
  • @AdrianRadulescu In case of get the request does not contain a body that's why you don't see that problem. I will check that repo later today. – Peter Csala Feb 07 '23 at 12:12
  • 1
    @AdrianRadulescu I've checked it and I don't see any reason why do you have the observed behaviour. It seems like you recreate the disposables rather than reusing them (which is good), so I'm out of ideas. Sorry :( – Peter Csala Feb 07 '23 at 13:52