Search code examples
c#dotnet-httpclientpollyretry-logic

The stream was already consumed. It cannot be read again error using Polly to retry requests C# .NET 6


I read this 'Stream was already consumed' error using Polly to retry requests in ASP.NET Core but cannot see where to clone the http request message.

So in my code, I register httpclient like:

var httpClientBuilder = serviceCollection.AddHttpClient("CustomHttpClientName");
httpClientBuilder.AddHttpMessageHandler(ctx=>...)
httpClientBuilder.AddPolicyHandler(GetRetryPolicy());
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions.HandleTransientHttpError()
        .OrResult(result => !result.IsSuccessStatusCode)
        .OrResult(result =>
        {
            var elem = result.Content.ReadFromJsonAsync<JsonElement>().GetAwaiter().GetResult();
            return elem.TryGetProperty("error", out _);
        })
        .WaitAndRetryAsync(
            sleepDurations: Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 3),
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                context.GetLogger()?.LogWarning("Delaying for {Delay}ms, then making retry {Retry}.", timespan.TotalMilliseconds, retryCount);
            });
}

and then http client call:

var request = CreateRequest(jsonContent, HttpMethod.Post, uri);
await GetClient().SendAsync(request, cancellationToken); // here I get runtime exception

I want to do a retry if in response json I see error field.

Above code leads to InvalidOperationException: 'The stream was already consumed. It cannot be read again'


Solution

  • I haven't tested this, but it might be possible to capture the original result in memory as you're reading it like this:

            .OrResult(result =>
            {
                if(result.Content is not ByteArrayContent)
                {
                    var originalContent = result.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
                    result.Content = new ByteArrayContent(originalContent);
                }
                var elem = result.Content.ReadFromJsonAsync<JsonElement>().GetAwaiter().GetResult();
                return elem.TryGetProperty("error", out _);
            })
    

    By replacing the content with an in-memory copy, you'd allow for multiple reads over it. Just be aware that this changes some behaviors in your application: the consuming code wouldn't appear to "complete" the request until after the entire response has been read. But if you're expecting to read the response as JSON that's probably not a huge deal.

    Another possible way to address this would be to split out the responsibilities. The IAsyncPolicy<HttpResponseMessage> might focus just on the HTTP response itself (without looking at the Content). Then you could have a separate utility class for getting the specific sort of JSON response that you're looking for, and have it use an IAsyncPolicy<JsonElement> around the combined actions of retrieving the response and reading the JSON from it. Then your "handle" criteria would be looking at the JsonElement directly rather than trying to read the JSON from the content stream directly.

    Also note that Polly.Extensions.Http has been deprecated in favor of Microsoft.Extensions.Http.Resilience.