dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

Home Page:https://docs.microsoft.com/dotnet/core/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Question: reusing HttpResponseMessage/HttpContent

rjperes opened this issue · comments

I am currently trying to reuse the HttpResponseMessage and the inner HttpContent, for the purposes of caching, that are returned by a DelegatingHandler of an HttpClient.

My latest approach was to copy the existing HttpContent into a new one of a custom class (which uses a inner MemoryStream for storage), including all of its headers, and to replace the original one in the HttpResponseMessage. This custom HttpContent class does not Dispose its content, instead, it resets the inner MemoryStream Position to 0.

Let's leave for now any performance/memory concerns. While debugging, I inspect the contents of the custom HttpContent and its inner MemoryStream and they seem to have what was initially stored.
However, when returning it back, I always get a message about not being able to parse the JSON contents (JSON was stored initially in my example).

Here is the class:

`
class NoDisposeStreamContent : HttpContent
{
    private readonly Stream _stream = new MemoryStream();
    
    public NoDisposeStreamContent(HttpContent content)
    {
        content.CopyTo(_stream, null, default);

        foreach (var headers in content.Headers)
        {
            this.Headers.TryAddWithoutValidation(headers.Key, headers.Value);
        }

        Reset();
    }

    private void Reset()
    {
        _stream.Position = 0;
    }

    protected override void Dispose(bool disposing)
    {
        Reset();
    }

    protected async Task CopyToStreamAsync(Stream stream)
    {
        var buffer = new byte[4096];
        var count = await _stream.ReadAsync(buffer, 0, buffer.Length);

        while (count > 0)
        {
            await stream.WriteAsync(buffer, 0, count);
            count = await _stream.ReadAsync(buffer, 0, buffer.Length);
        }

        Reset();
    }

    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
    {
        await CopyToStreamAsync(stream);
    }

    protected override bool TryComputeLength(out long length)
    {
        if (_stream.CanSeek)
        {
            length = _stream.Length;
            return true;
        }

        length = 0;
        return false;
    }

    protected override Task<Stream> CreateContentReadStreamAsync()
    {
        return Task.FromResult(_stream);
    }
}
`

Any thoughts about this approach? What am I missing?
Many thanks!

This custom HttpContent class does not Dispose its content, instead, it resets the inner MemoryStream Position to 0.

Note that a Stream object can't be used concurrently. It holds the state about current position. When multiple responses are trying to load content from the stream, they will fail.

The best effort is to hold the backing array, and rent a new Stream from pool for each access. In Dispose, return the Stream object back to pool.

The memory pressure of the Stream object is very low comparing to the storage array. Caching the array itself may be enough.

@huoyaoyuan : thank you for your answer.
The Stream object is not used concurrently. It is used by possibly another instance of an HttpClient.

The Stream object is not used concurrently. It is used by possibly another instance of an HttpClient.

A HttpClient is allowed to be concurrent. How is it using the response?

@huoyaoyuan : that may well be, but not the issue that I'm facing, because I'm doing a simple test on my machine. I can share more details if there is need, but what do you think of this alternative solution?

`
class NoDisposeStreamContent : HttpContent
{
    private byte[] _buffer;

    public NoDisposeStreamContent(HttpContent content)
    {
        _buffer = content.ReadAsByteArrayAsync().ConfigureAwait(false).GetAwaiter().GetResult();

        foreach (var headers in content.Headers)
        {
            this.Headers.TryAddWithoutValidation(headers.Key, headers.Value);
        }
    }

    protected override void Dispose(bool disposing)
    {
    }

    protected async Task CopyToStreamAsync(Stream stream)
    {
        await stream.WriteAsync(_buffer, 0, _buffer.Length);
    }

    protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
    {
        await CopyToStreamAsync(stream);
    }

    protected override bool TryComputeLength(out long length)
    {
        length = _buffer.Length;
        return true;
    }

    protected override Task<Stream> CreateContentReadStreamAsync()
    {
        return Task.FromResult<Stream>(new MemoryStream(_buffer));
    }
}
`

I think you need to share more of what you're trying to do (code (handler, how you call HttpClient), descriptions and exceptions). Also HttpRequest/ResponseMessage objects are not meant to be re-used, e.g. once the response message gets disposed it will start throwing from all its methods.

In a nutshell, I want to create a CachingHandler, that inherits from DelegatingHandler, and stores the HttpResponseMessage in a IMemoryCache using the HttpRequestMessage.RequestUri as the key. I got it working, I just had to replace the HttpResponseMessage.Content with a custom class that does not Dispose and stores the contents (and headers) of the original Content. Similar to the code I posted, with the addition of a NoDisposeMemoryStream (returned from CreateContentReadStreamAsync) that does just what it says. Seems to be working.

If it's working, closing then.

@rjperes have you considered just employing output caching on the API side instead? Even if you don't control the API, creating a caching proxy would be much easier to accomplish than what you are trying to do IMHO.

@julealgon : Oi, Juliano. Yes, but that is not what I'm looking for, this is more of a technical challenge, I was trying to see if it was possible. If you're curious, you can see the result here: https://weblogs.asp.net/ricardoperes/caching-httpclient-requests.