Adrian Radulescu
Adrian Radulescu

Reputation: 25

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

Upvotes: 2

Views: 896

Answers (2)

Wallace B. McClure
Wallace B. McClure

Reputation: 1545

I am merely putting this out there because I ran across this stream closed error when I was doing web service calls with post in maui against a web service that I have written in azure. I'm converting some old code from xamarin to maui. I pulled my hair out for a couple of hours. then I looked at the server name and protocol I was using. I had inadvertently used http as the protocol. I switched to using https and all of the errors went away.

Upvotes: 0

Peter Csala
Peter Csala

Reputation: 22819

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

        ...
    }

    ...
}

Upvotes: 2

Related Questions