diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs index 70f96d7537..19b77538f9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs @@ -19,6 +19,10 @@ namespace Microsoft.AspNetCore.ResponseCaching private ResponseHeaders _responseHeaders; private CacheControlHeaderValue _requestCacheControl; private CacheControlHeaderValue _responseCacheControl; + private DateTimeOffset? _responseDate; + private bool _parsedResponseDate; + private DateTimeOffset? _responseExpires; + private bool _parsedResponseExpires; internal ResponseCacheContext( HttpContext httpContext) @@ -101,5 +105,37 @@ namespace Microsoft.AspNetCore.ResponseCaching return _responseCacheControl; } } + + internal DateTimeOffset? ResponseDate + { + get + { + if (!_parsedResponseDate) + { + _parsedResponseDate = true; + _responseDate = TypedResponseHeaders.Date; + } + return _responseDate; + } + set + { + // Don't reparse the response date again if it's explicitly set + _parsedResponseDate = true; + _responseDate = value; + } + } + + internal DateTimeOffset? ResponseExpires + { + get + { + if (!_parsedResponseExpires) + { + _parsedResponseExpires = true; + _responseExpires = TypedResponseHeaders.Expires; + } + return _responseExpires; + } + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 850bfdb236..8697aecc5c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (body.Length > 0) { // Add a content-length if required - if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) { response.ContentLength = body.Length; } @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? - (context.TypedResponseHeaders.Expires - context.ResponseTime) ?? + (context.ResponseExpires - context.ResponseTime) ?? DefaultExpirationTimeSpan; // Check if any vary rules exist @@ -234,16 +234,18 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Ensure date header is set - if (context.TypedResponseHeaders.Date == null) + if (!context.ResponseDate.HasValue) { - context.TypedResponseHeaders.Date = context.ResponseTime; + context.ResponseDate = context.ResponseTime; + // Setting the date on the raw response headers. + context.TypedResponseHeaders.Date = context.ResponseDate; } // Store the response on the state context.CachedResponse = new CachedResponse { BodyKeyPrefix = FastGuid.NewGuid().IdString, - Created = context.TypedResponseHeaders.Date.Value, + Created = context.ResponseDate.Value, StatusCode = context.HttpContext.Response.StatusCode }; @@ -263,10 +265,10 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) { + var contentLength = context.TypedResponseHeaders.ContentLength; if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled && - (context.TypedResponseHeaders.ContentLength == null || - context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length)) + (!contentLength.HasValue || contentLength == context.ResponseCacheStream.BufferedStream.Length)) { if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) { @@ -355,9 +357,13 @@ namespace Microsoft.AspNetCore.ResponseCaching } } } - else if (context.TypedRequestHeaders.IfUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) + else { - return true; + var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; + if (ifUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= ifUnmodifiedSince) + { + return true; + } } return false; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index 912e6b48a4..fc6d51dae9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -100,35 +100,35 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Check response freshness - if (context.TypedResponseHeaders.Date == null) + if (!context.ResponseDate.HasValue) { - if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && - context.ResponseCacheControlHeaderValue.MaxAge == null && - context.ResponseTime > context.TypedResponseHeaders.Expires) + if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && + !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && + context.ResponseTime >= context.ResponseExpires) { return false; } } else { - var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value; + var age = context.ResponseTime - context.ResponseDate.Value; // Validate shared max age - if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge) + if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge) { return false; } - else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null) + else if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue) { // Validate max age - if (age > context.ResponseCacheControlHeaderValue.MaxAge) + if (age >= context.ResponseCacheControlHeaderValue.MaxAge) { return false; } - else if (context.ResponseCacheControlHeaderValue.MaxAge == null) + else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime > context.TypedResponseHeaders.Expires) + if (context.ResponseTime >= context.ResponseExpires) { return false; } @@ -145,21 +145,21 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements - if (context.RequestCacheControlHeaderValue.MinFresh != null) + if (context.RequestCacheControlHeaderValue.MinFresh.HasValue) { age += context.RequestCacheControlHeaderValue.MinFresh.Value; } // Validate shared max age, this overrides any max age settings for shared caches - if (age > cachedControlHeaders.SharedMaxAge) + if (age >= cachedControlHeaders.SharedMaxAge) { // shared max age implies must revalidate return false; } - else if (cachedControlHeaders.SharedMaxAge == null) + else if (!cachedControlHeaders.SharedMaxAge.HasValue) { // Validate max age - if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge) + if (age >= cachedControlHeaders.MaxAge || age >= context.RequestCacheControlHeaderValue.MaxAge) { // Must revalidate if (cachedControlHeaders.MustRevalidate) @@ -175,10 +175,10 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null) + else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime > context.CachedResponseHeaders.Expires) + if (context.ResponseTime >= context.CachedResponseHeaders.Expires) { return false; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index fb1b817e57..0afbf329e3 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -290,7 +290,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsResponseCacheable_PastExpiry_NotAllowed() + public void IsResponseCacheable_AtExpiry_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; @@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = DateTimeOffset.MaxValue; + context.ResponseTime = utcNow; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -338,7 +338,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() + public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) }; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -408,17 +408,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsCachedEntryFresh_PastExpiry_IsNotFresh() + public void IsCachedEntryFresh_AtExpiry_IsNotFresh() { + var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.ResponseTime = DateTimeOffset.MaxValue; + context.ResponseTime = utcNow; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { Public = true }, - Expires = DateTimeOffset.UtcNow + Expires = utcNow }; Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); @@ -449,7 +450,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -490,7 +491,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -512,7 +513,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { - MinFresh = TimeSpan.FromSeconds(3) + MinFresh = TimeSpan.FromSeconds(2) }; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -542,7 +543,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), } }; - context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } @@ -569,6 +570,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } + [Fact] + public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() + { + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(6) + }; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + } + }; + context.CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + } + [Fact] public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { @@ -591,27 +614,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } - - [Fact] - public void IsCachedEntryFresh_IgnoresRequestVerificationWhenSpecified() - { - var context = TestUtils.CreateTestContext(); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() - { - MinFresh = TimeSpan.FromSeconds(1), - MaxAge = TimeSpan.FromSeconds(3) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - } - }; - context.CachedEntryAge = TimeSpan.FromSeconds(3); - - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); - } } }