.net.net-6.0dotnet-httpclientdelegatinghandler

The stream was already consumed. It cannot be read again. When trying to clone HttpResponseMessage with the message delegating handler


I tried cloning the HttpResponseMessage object and printing the response in a fire-and-forget async call. For this, I received The stream already consumed error.

Any leads are appreciated.

Logs

 An unhandled exception has occurred while executing the request.
      System.InvalidOperationException: The stream was already consumed. It cannot be read again.
         at System.Net.Http.HttpConnectionResponseContent.ConsumeStream()
         at System.Net.Http.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)

Sample Code


using System.Text;
using BookMyShow.Logging;
using Microsoft.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient();
builder.Services.ConfigureAll<HttpClientFactoryOptions>(options =>
{
    options.HttpMessageHandlerBuilderActions.Add(messageHandlerBuilder =>
    {
        messageHandlerBuilder.AdditionalHandlers.Add(messageHandlerBuilder.Services.GetRequiredService<HttpRetentionHandler>());
    });
});
builder.Services.AddTransient<HttpRetentionHandler>();
var app = builder.Build();

app.MapPost("/post", async (IHttpClientFactory httpClientFactory) =>
{
    var httpClient = httpClientFactory.CreateClient();
    string jsonPayload = @"{""title"": ""New Post"", ""body"": ""This is the body of the new post"", ""userId"": 1}";
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
    using var request = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("https://jsonplaceholder.typicode.com/posts"),
        Content = content
    };
    var response = await httpClient.SendAsync(request);
        
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine($"Integration Service failed with error and Status Code: {response.StatusCode}");
    }
    string body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    return Results.Ok(body);
});

app.Run();

public class HttpRetentionHandler : DelegatingHandler
{
    public HttpRetentionHandler()
    {
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

        var logReference = new Dictionary<string, object>();
        try
        {
            HttpResponseMessage responseMessage = await base.SendAsync(request, cancellationToken);
            HttpResponseMessage clonedResponse = await CloneAsync(responseMessage);

            var responseText = new StringBuilder(clonedResponse.ToString());
            responseText.Append('\n');
            logReference["response"] = responseText.ToString();
            Task.Run(async () => {
                await PrintApiResponseAsync("print response", logReference, clonedResponse.Content, responseText);
            }, cancellationToken);
            return responseMessage;

        }
        catch (Exception)
        {
            throw;
        }
    }
        


    async Task PrintApiResponseAsync(string name, Dictionary<string, object> logReference, HttpContent responseContent, StringBuilder responseText)
    {

        if (responseContent != null)
        {
            string content = await responseContent.ReadAsStringAsync();
            responseText.Append(content);
            logReference["response"] = responseText.ToString();
        }
        string text = JsonSerializer.Serialize(logReference, new JsonSerializerOptions { WriteIndented = true });
        Console.WriteLine(text);
    }
        
    private async Task<HttpResponseMessage> CloneAsync(HttpResponseMessage response)
    {
        var clone = new HttpResponseMessage(response.StatusCode);
            
        if (response.Content != null)
        {
            using var ms = new MemoryStream();
            await response.Content.CopyToAsync(ms);
                
            ms.Position = 0;
            clone.Content = new StreamContent(ms);

            response.Content.Headers.ToList().ForEach(header =>
                clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value));
        }
        response.Headers.ToList().ForEach(header =>
            clone.Headers.TryAddWithoutValidation(header.Key, header.Value));
        return clone;
    }
}

Solution

  • As  Peter Csala suggested, I solved it using LoadIntoBufferAsync()

    
    using System.Text;
    using BookMyShow.Logging;
    using Microsoft.Extensions.Http;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddHttpClient();
    builder.Services.ConfigureAll<HttpClientFactoryOptions>(options =>
    {
        options.HttpMessageHandlerBuilderActions.Add(messageHandlerBuilder =>
        {
            messageHandlerBuilder.AdditionalHandlers.Add(messageHandlerBuilder.Services.GetRequiredService<HttpRetentionHandler>());
        });
    });
    builder.Services.AddTransient<HttpRetentionHandler>();
    var app = builder.Build();
    
    app.MapPost("/post", async (IHttpClientFactory httpClientFactory) =>
    {
        var httpClient = httpClientFactory.CreateClient();
        string jsonPayload = @"{""title"": ""New Post"", ""body"": ""This is the body of the new post"", ""userId"": 1}";
        var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
        using var request = new HttpRequestMessage
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri("https://jsonplaceholder.typicode.com/posts"),
            Content = content
        };
        var response = await httpClient.SendAsync(request);
            
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine($"Integration Service failed with error and Status Code: {response.StatusCode}");
        }
        string body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        return Results.Ok(body);
    });
    
    app.Run();
    
    public class HttpRetentionHandler : DelegatingHandler
    {
        public HttpRetentionHandler()
        {
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
    
            var logReference = new Dictionary<string, object>();
            try
            {
                HttpResponseMessage responseMessage = await base.SendAsync(request, cancellationToken);
    
                var responseText = new StringBuilder(clonedResponse.ToString());
                responseText.Append('\n');
                await responseMessage.Content.LoadIntoBufferAsync()!;
                string responseContent = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
                if (responseContent.IsNotEmpty())
                {
                    responseText.Append(responseContent);
                  }
                logReference["response"] = responseText.ToString();
                Task.Run(async () => {
                    await PrintApiResponseAsync("print response", logReference, clonedResponse.Content, responseText);
                }, cancellationToken);
                return responseMessage;
    
            }
            catch (Exception)
            {
                throw;
            }
        }
            
    
    
        async Task PrintApiResponseAsync(string name, Dictionary<string, object> logReference, HttpContent responseContent, StringBuilder responseText)
        {
    
            if (responseContent != null)
            {
                string content = await responseContent.ReadAsStringAsync();
                responseText.Append(content);
                logReference["response"] = responseText.ToString();
            }
            string text = JsonSerializer.Serialize(logReference, new JsonSerializerOptions { WriteIndented = true });
            Console.WriteLine(text);
        }
    }