diff --git a/.gitignore b/.gitignore index 767af77..ff41a39 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ artifacts/ script.sql .idea/ .vs/ -TestResults/ \ No newline at end of file +TestResults/ +.vscode/ \ No newline at end of file diff --git a/src/CacheCow.Client/CachingHandler.cs b/src/CacheCow.Client/CachingHandler.cs index ee4baf7..49cdd9b 100644 --- a/src/CacheCow.Client/CachingHandler.cs +++ b/src/CacheCow.Client/CachingHandler.cs @@ -25,11 +25,11 @@ public class CachingHandler : DelegatingHandler // 13.4: A response received with a status code of 200, 203, 206, 300, 301 or 410 MAY be stored // TODO: Implement caching statuses other than 2xx private static HttpStatusCode[] _cacheableStatuses = new HttpStatusCode[] - { - HttpStatusCode.OK, HttpStatusCode.NonAuthoritativeInformation, - HttpStatusCode.PartialContent, HttpStatusCode.MultipleChoices, - HttpStatusCode.MovedPermanently, HttpStatusCode.Gone - }; + { + HttpStatusCode.OK, HttpStatusCode.NonAuthoritativeInformation, + HttpStatusCode.PartialContent, HttpStatusCode.MultipleChoices, + HttpStatusCode.MovedPermanently, HttpStatusCode.Gone + }; public CachingHandler() : this(new InMemoryCacheStore()) @@ -338,7 +338,7 @@ protected async override Task SendAsync(HttpRequestMessage if (isFreshOrStaleAcceptable.HasValue && isFreshOrStaleAcceptable.Value) // similar to OK { // TODO: CONSUME AND RELEASE Response !!! - if (! DoNotEmitCacheCowHeader) + if (!DoNotEmitCacheCowHeader) cachedResponse.AddCacheCowHeader(cacheCowHeader); return cachedResponse; // EXIT !! ____________________________ @@ -357,6 +357,15 @@ protected async override Task SendAsync(HttpRequestMessage // _______________________________ RESPONSE only GET ___________________________________________ var serverResponse = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (serverResponse.Content != null) + { + // these two prevent serialisation without ContentLength which barfs for chunked encoding - issue #267 + TraceWriter.WriteLine($"Content Size: {serverResponse.Content.Headers.ContentLength}", TraceLevel.Verbose); + if (serverResponse.Content.Headers.ContentType == null) + { + serverResponse.Content.Headers.Add("Content-Type", "application/octet-stream"); + } + } // HERE IS LATE FOR APPLYING EXCEPTION POLICY !!! @@ -380,7 +389,7 @@ protected async override Task SendAsync(HttpRequestMessage await UpdateCachedResponseAsync(cacheKey, cachedResponse, serverResponse, _cacheStore).ConfigureAwait(false); ConsumeAndDisposeResponse(serverResponse); - if (! DoNotEmitCacheCowHeader) + if (!DoNotEmitCacheCowHeader) cachedResponse.AddCacheCowHeader(cacheCowHeader).CopyOtherCacheCowHeaders(serverResponse); return cachedResponse; // EXIT !! _______________ @@ -442,7 +451,7 @@ protected async override Task SendAsync(HttpRequestMessage TraceWriter.WriteLine("{0} - Before returning response", TraceLevel.Verbose, request.RequestUri.ToString()); - if (! DoNotEmitCacheCowHeader) + if (!DoNotEmitCacheCowHeader) serverResponse.AddCacheCowHeader(cacheCowHeader); return serverResponse; @@ -495,7 +504,7 @@ internal async static Task UpdateCachedResponseAsync(CacheKey cacheKey, private static void CheckForCacheCowHeader(HttpResponseMessage responseMessage) { var header = responseMessage.Headers.GetCacheCowHeader(); - if (header!=null) + if (header != null) { TraceWriter.WriteLine("!!WARNING!! response stored with CacheCowHeader!!", TraceLevel.Warning); } diff --git a/src/CacheCow.Client/InMemoryCacheStore.cs b/src/CacheCow.Client/InMemoryCacheStore.cs index 0882721..3723a1b 100644 --- a/src/CacheCow.Client/InMemoryCacheStore.cs +++ b/src/CacheCow.Client/InMemoryCacheStore.cs @@ -67,40 +67,43 @@ public InMemoryCacheStore(TimeSpan minExpiry, IOptions optio #endif - /// + /// public void Dispose() - { - _responseCache.Dispose(); - } + { + _responseCache.Dispose(); + } /// public async Task GetValueAsync(CacheKey key) - { - var result = _responseCache.Get(key.HashBase64); - if (result == null) - return null; + { + var result = (byte[])_responseCache.Get(key.HashBase64); + if (result == null) + return null; - return await _messageSerializer.DeserializeToResponseAsync(new MemoryStream((byte[]) result)).ConfigureAwait(false); - } + return await _messageSerializer.DeserializeToResponseAsync(new MemoryStream(result)).ConfigureAwait(false); + } /// public async Task AddOrUpdateAsync(CacheKey key, HttpResponseMessage response) - { + { // removing reference to request so that the request can get GCed + // UPDATE 2022 - What on earth am I doing it for? I cannot remember. var req = response.RequestMessage; response.RequestMessage = null; var memoryStream = new MemoryStream(); - await _messageSerializer.SerializeAsync(response, memoryStream).ConfigureAwait(false); + await _messageSerializer.SerializeAsync(response, memoryStream).ConfigureAwait(false); + var buffer = memoryStream.ToArray(); + response.RequestMessage = req; var suggestedExpiry = response.GetExpiry() ?? DateTimeOffset.UtcNow.Add(_minExpiry); var minExpiry = DateTimeOffset.UtcNow.Add(_minExpiry); var optimalExpiry = (suggestedExpiry > minExpiry) ? suggestedExpiry : minExpiry; - _responseCache.Set(key.HashBase64, memoryStream.ToArray(), optimalExpiry); - } + _responseCache.Set(key.HashBase64, buffer, optimalExpiry); + } /// public Task TryRemoveAsync(CacheKey key) - { + { #if NET452 return Task.FromResult(_responseCache.Remove(key.HashBase64) != null); #else @@ -111,14 +114,14 @@ public Task TryRemoveAsync(CacheKey key) /// public Task ClearAsync() - { + { _responseCache.Dispose(); #if NET452 _responseCache = new MemoryCache(CacheStoreEntryName); #else _responseCache = new MemoryCache(_options); #endif - return Task.FromResult(0); - } - } + return Task.FromResult(0); + } + } } diff --git a/src/CacheCow.Client/MessageContentHttpMessageSerializer.cs b/src/CacheCow.Client/MessageContentHttpMessageSerializer.cs index a929e22..7c9a3a8 100644 --- a/src/CacheCow.Client/MessageContentHttpMessageSerializer.cs +++ b/src/CacheCow.Client/MessageContentHttpMessageSerializer.cs @@ -39,9 +39,12 @@ public async Task SerializeAsync(HttpResponseMessage response, Stream stream) { TraceWriter.WriteLine("SerializeAsync - before load", TraceLevel.Verbose); - if(_bufferContent) + // this will prevent serialisation without ContentLength which barfs for chunked encoding - issue #267 + var contentLength = response.Content.Headers.ContentLength; + + if (_bufferContent) await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); - TraceWriter.WriteLine("SerializeAsync - after load", TraceLevel.Verbose); + TraceWriter.WriteLine("SerializeAsync - after load", TraceLevel.Verbose); } else { @@ -51,6 +54,7 @@ public async Task SerializeAsync(HttpResponseMessage response, Stream stream) var httpMessageContent = new HttpMessageContent(response); var buffer = await httpMessageContent.ReadAsByteArrayAsync(); + TraceWriter.WriteLine("SerializeAsync - after ReadAsByteArrayAsync", TraceLevel.Verbose); stream.Write(buffer, 0, buffer.Length); } @@ -64,7 +68,7 @@ public async Task SerializeAsync(HttpRequestMessage request, Stream stream) var httpMessageContent = new HttpMessageContent(request); var buffer = await httpMessageContent.ReadAsByteArrayAsync().ConfigureAwait(false); - stream.Write(buffer, 0, buffer.Length); + stream.Write(buffer, 0, buffer.Length); } public async Task DeserializeToResponseAsync(Stream stream) @@ -76,7 +80,13 @@ public async Task DeserializeToResponseAsync(Stream stream) TraceLevel.Verbose); var responseMessage = await response.Content.ReadAsHttpResponseMessageAsync().ConfigureAwait(false); if (responseMessage.Content != null && _bufferContent) - await responseMessage.Content.LoadIntoBufferAsync().ConfigureAwait(false); + { + await responseMessage.Content.LoadIntoBufferAsync().ConfigureAwait(false); + } + + if (responseMessage.Content == null) + TraceWriter.WriteLine("Content is NULL desering from cache", TraceLevel.Warning); + return responseMessage; } diff --git a/test/CacheCow.Client.Tests/IntegrationTests.cs b/test/CacheCow.Client.Tests/IntegrationTests.cs index 3506d2c..beecd2f 100644 --- a/test/CacheCow.Client.Tests/IntegrationTests.cs +++ b/test/CacheCow.Client.Tests/IntegrationTests.cs @@ -3,29 +3,31 @@ using CacheCow.Client.Headers; using CacheCow.Common; using Xunit; +using System; +using System.IO; namespace CacheCow.Client.Tests { public class IntegrationTests - { + { public const string Url = "https://ssl.gstatic.com/gb/images/j_e6a6aca6.png"; - [Fact] - public async Task Test_GoogleImage_WorksOnFirstSecondRequestNotThird() - { - var httpClient = new HttpClient(new CachingHandler() - { - InnerHandler = new HttpClientHandler() - }); + [Fact] + public async Task Test_GoogleImage_WorksOnFirstSecondRequestNotThird() + { + var httpClient = new HttpClient(new CachingHandler() + { + InnerHandler = new HttpClientHandler() + }); httpClient.DefaultRequestHeaders.Add(HttpHeaderNames.Accept, "image/png"); - var httpResponseMessage = await httpClient.GetAsync(Url); - var httpResponseMessage2 = await httpClient.GetAsync(Url); - var cacheCowHeader = httpResponseMessage2.Headers.GetCacheCowHeader(); - Assert.NotNull(cacheCowHeader); - Assert.Equal(true, cacheCowHeader.RetrievedFromCache); - } + var httpResponseMessage = await httpClient.GetAsync(Url); + var httpResponseMessage2 = await httpClient.GetAsync(Url); + var cacheCowHeader = httpResponseMessage2.Headers.GetCacheCowHeader(); + Assert.NotNull(cacheCowHeader); + Assert.Equal(true, cacheCowHeader.RetrievedFromCache); + } [Fact] public async Task Simple_Caching_Example_From_Issue263() @@ -35,9 +37,35 @@ public async Task Simple_Caching_Example_From_Issue263() var response = await client.GetAsync(CacheableResource); var responseFromCache = await client.GetAsync(CacheableResource); Assert.Equal(true, response.Headers.GetCacheCowHeader().DidNotExist); - Assert.Equal(true, responseFromCache.Headers.GetCacheCowHeader().RetrievedFromCache); + Assert.Equal(true, responseFromCache.Headers.GetCacheCowHeader()?.RetrievedFromCache); + } + + + [Fact] // Skip if the resource becomes unavailable + public async Task Simple_Caching_Example_From_Issue267() + { + var client = ClientExtensions.CreateClient(); + + // this one does not have a content-type too and that could be somehow related + // but not quite sure. Have used other places where a chunked encoding might + // be returned but did not cause the same problem + const string CacheableResource = "https://webhooks.truelayer-sandbox.com/.well-known/jwks"; + + var response = await client.GetAsync(CacheableResource); + var body = await response.Content.ReadAsByteArrayAsync(); + var responseFromCache = await client.GetAsync(CacheableResource); + if (responseFromCache.Content == null) + { + throw new InvalidOperationException("Response content from cache is null"); + } + + var bodyFromCache = await responseFromCache.Content.ReadAsByteArrayAsync(); + Assert.Equal(true, response.Headers.GetCacheCowHeader()?.DidNotExist); + Assert.Equal(body.Length, bodyFromCache.Length); } + + [Fact] public async Task SettingNoHeaderWorks() { @@ -59,5 +87,5 @@ public async Task SettingNoHeaderWorks() Assert.Null(h); } - } + } } diff --git a/test/CacheCow.Client.Tests/ResponseSerializationTests.cs b/test/CacheCow.Client.Tests/ResponseSerializationTests.cs index a6f1bae..b2d9cf2 100644 --- a/test/CacheCow.Client.Tests/ResponseSerializationTests.cs +++ b/test/CacheCow.Client.Tests/ResponseSerializationTests.cs @@ -11,12 +11,12 @@ namespace CacheCow.Client.Tests { - - public class ResponseSerializationTests - { - [Fact] - public async Task IntegrationTest_Deserialize() - { + + public class ResponseSerializationTests + { + [Fact] + public async Task IntegrationTest_Deserialize() + { var httpClient = new HttpClient(); var httpResponseMessage = await httpClient.GetAsync(IntegrationTests.Url); Console.WriteLine(httpResponseMessage.Headers.ToString()); @@ -26,30 +26,30 @@ public async Task IntegrationTest_Deserialize() fileStream.Close(); var fileStream2 = new FileStream("msg.bin", FileMode.Open); - var httpResponseMessage2 = await defaultHttpResponseMessageSerializer.DeserializeToResponseAsync(fileStream2); - fileStream.Close(); - } + var httpResponseMessage2 = await defaultHttpResponseMessageSerializer.DeserializeToResponseAsync(fileStream2); + fileStream.Close(); + } - [Fact] - public async Task IntegrationTest_Serialize_Deserialize() - { - var httpClient = new HttpClient(); - var httpResponseMessage = await httpClient.GetAsync(IntegrationTests.Url); - var contentLength = httpResponseMessage.Content.Headers.ContentLength; // access to make sure is populated http://aspnetwebstack.codeplex.com/discussions/388196 - var memoryStream = new MemoryStream(); - var defaultHttpResponseMessageSerializer = new MessageContentHttpMessageSerializer(); - await defaultHttpResponseMessageSerializer.SerializeAsync(httpResponseMessage, memoryStream); - memoryStream.Position = 0; - var httpResponseMessage2 = await defaultHttpResponseMessageSerializer.DeserializeToResponseAsync(memoryStream); - Assert.Equal(httpResponseMessage.StatusCode, httpResponseMessage2.StatusCode); - Assert.Equal(httpResponseMessage.ReasonPhrase, httpResponseMessage2.ReasonPhrase); - Assert.Equal(httpResponseMessage.Version, httpResponseMessage2.Version); - Assert.Equal(httpResponseMessage.Headers.ToString(), httpResponseMessage2.Headers.ToString()); - Assert.Equal(await httpResponseMessage.Content.ReadAsStringAsync(), - await httpResponseMessage2.Content.ReadAsStringAsync()); - Assert.Equal(httpResponseMessage.Content.Headers.ToString(), - httpResponseMessage2.Content.Headers.ToString()); + [Fact] + public async Task IntegrationTest_Serialize_Deserialize() + { + var httpClient = new HttpClient(); + var httpResponseMessage = await httpClient.GetAsync("https://webhooks.truelayer-sandbox.com/.well-known/jwks"); + var memoryStream = new MemoryStream(); + + var defaultHttpResponseMessageSerializer = new MessageContentHttpMessageSerializer(); + await defaultHttpResponseMessageSerializer.SerializeAsync(httpResponseMessage, memoryStream); - } - } + memoryStream.Position = 0; + var httpResponseMessage2 = await defaultHttpResponseMessageSerializer.DeserializeToResponseAsync(memoryStream); + Assert.Equal(httpResponseMessage.StatusCode, httpResponseMessage2.StatusCode); + Assert.Equal(httpResponseMessage.ReasonPhrase, httpResponseMessage2.ReasonPhrase); + Assert.Equal(httpResponseMessage.Version, httpResponseMessage2.Version); + Assert.Equal(httpResponseMessage.Headers.ToString(), httpResponseMessage2.Headers.ToString()); + Assert.Equal(await httpResponseMessage.Content.ReadAsStringAsync(), + await httpResponseMessage2.Content.ReadAsStringAsync()); + Assert.Equal(httpResponseMessage.Content.Headers.ToString(), + httpResponseMessage2.Content.Headers.ToString()); + } + } }