Search code examples
c#redishttpresponsemessage

Caching Serialized HttpResponseMessage to Redis. Error on read. "InvalidOperationException: The stream was already consumed. It cannot be read again."


I have a function that takes an API request, checks if the result exists in a Redis cache, if it does it returns the cached value, if not it sends the API request and then caches the value.

private async Task<HttpResponseMessage> RestGetCachedAsync(string query, ILogger logger = null)
    {
        string key = $"GET:{query}";
        HttpResponseMessage response;

        var cacheResponse = await _cacheService.GetStringValue(key);

        if (cacheResponse != null)
        {
            response = JsonConvert.DeserializeObject<HttpResponseMessage>(cacheResponse, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                NullValueHandling = NullValueHandling.Ignore,
            });

            if(response.IsSuccessStatusCode) return response;
        }

        response = await RestGetAsync(query, logger);

        if (response.IsSuccessStatusCode)
        {
            await _cacheService.SetStringValue(key, JsonConvert.SerializeObject(response, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                NullValueHandling = NullValueHandling.Ignore,
            }));
        }            

        return response;
    }

When reading a previously queried API

public async Task<string> DeserializeHttpResponse(HttpResponseMessage response)
    {
        return await response.Content.ReadAsStringAsync();
    }

I get the following error.

InvalidOperationException: The stream was already consumed. It cannot be read again.

Solution

  • After a discussion in the comments to the question I realized my fault. I thought that the content data was stored with the HttpResponseMessage and would be there if I serialized and deserialized. However, it looks like the Content data in the HttpResponseMessage is more like a pointer that provides instructions for how to read the value which is stored elsewhere and these instructions are invoked by the ReadAsStringAsync() function in the HttpContent class.

    So my quick fix was to create a wrapper object that stores the serialized HttpResponseMessage as well as the Content result returned by ReadAsStringAsync(). This wrapper looks like this.

    public class WrapperHttpResponse
    {
        public HttpResponseMessage HttpResponseMessage { get; set; }
        public string Content { get; set; }
    
        public WrapperHttpResponse()
        {
    
        }
    
        public WrapperHttpResponse(HttpResponseMessage httpResponseMessage)
        {
            HttpResponseMessage = httpResponseMessage;
            Content = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        }
    
        public WrapperHttpResponse(HttpResponseMessage httpResponseMessage, string content)
        {
            HttpResponseMessage = httpResponseMessage;
            Content = content;
        }
    }
    

    this method has 3 constructors which allows me to instantiate null, read, and unread instances of HttpResponseMessages. Then I rewrote my caching execution as follows.

    private async Task<WrapperHttpResponse> RestGetCachedAsync(string query, ILogger logger = null)
        {
            string key = $"GET:{query}";
            WrapperHttpResponse response;
    
            var cacheResponse = await _cacheService.GetStringValue(key);
    
            if (cacheResponse != null)
            {
                response = JsonConvert.DeserializeObject<WrapperHttpResponse>(cacheResponse, new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto,
                    NullValueHandling = NullValueHandling.Ignore,
                });
    
                if(response.HttpResponseMessage.IsSuccessStatusCode) return response;
            }
    
            response = await RestGetAsync(query, logger);
    
            if (response.HttpResponseMessage.IsSuccessStatusCode)
            {
                await _cacheService.SetStringValue(key, JsonConvert.SerializeObject(response, new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto,
                    NullValueHandling = NullValueHandling.Ignore,
                }));
            }            
    
            return response;
        }