Stricter expiration checks to avoid serving responses when max-age is 0

Cache parsed response headers for performance
This commit is contained in:
John Luo 2016-09-20 12:32:51 -07:00
parent c30d471c27
commit 6891d00032
4 changed files with 102 additions and 59 deletions

View File

@ -19,6 +19,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
private ResponseHeaders _responseHeaders; private ResponseHeaders _responseHeaders;
private CacheControlHeaderValue _requestCacheControl; private CacheControlHeaderValue _requestCacheControl;
private CacheControlHeaderValue _responseCacheControl; private CacheControlHeaderValue _responseCacheControl;
private DateTimeOffset? _responseDate;
private bool _parsedResponseDate;
private DateTimeOffset? _responseExpires;
private bool _parsedResponseExpires;
internal ResponseCacheContext( internal ResponseCacheContext(
HttpContext httpContext) HttpContext httpContext)
@ -101,5 +105,37 @@ namespace Microsoft.AspNetCore.ResponseCaching
return _responseCacheControl; 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;
}
}
} }
} }

View File

@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (body.Length > 0) if (body.Length > 0)
{ {
// Add a content-length if required // 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; response.ContentLength = body.Length;
} }
@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty;
context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ??
context.ResponseCacheControlHeaderValue.MaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ??
(context.TypedResponseHeaders.Expires - context.ResponseTime) ?? (context.ResponseExpires - context.ResponseTime) ??
DefaultExpirationTimeSpan; DefaultExpirationTimeSpan;
// Check if any vary rules exist // Check if any vary rules exist
@ -234,16 +234,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// Ensure date header is set // 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 // Store the response on the state
context.CachedResponse = new CachedResponse context.CachedResponse = new CachedResponse
{ {
BodyKeyPrefix = FastGuid.NewGuid().IdString, BodyKeyPrefix = FastGuid.NewGuid().IdString,
Created = context.TypedResponseHeaders.Date.Value, Created = context.ResponseDate.Value,
StatusCode = context.HttpContext.Response.StatusCode StatusCode = context.HttpContext.Response.StatusCode
}; };
@ -263,10 +265,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context)
{ {
var contentLength = context.TypedResponseHeaders.ContentLength;
if (context.ShouldCacheResponse && if (context.ShouldCacheResponse &&
context.ResponseCacheStream.BufferingEnabled && context.ResponseCacheStream.BufferingEnabled &&
(context.TypedResponseHeaders.ContentLength == null || (!contentLength.HasValue || contentLength == context.ResponseCacheStream.BufferedStream.Length))
context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length))
{ {
if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) 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; return false;

View File

@ -100,35 +100,35 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// Check response freshness // Check response freshness
if (context.TypedResponseHeaders.Date == null) if (!context.ResponseDate.HasValue)
{ {
if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue &&
context.ResponseCacheControlHeaderValue.MaxAge == null && !context.ResponseCacheControlHeaderValue.MaxAge.HasValue &&
context.ResponseTime > context.TypedResponseHeaders.Expires) context.ResponseTime >= context.ResponseExpires)
{ {
return false; return false;
} }
} }
else else
{ {
var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value; var age = context.ResponseTime - context.ResponseDate.Value;
// Validate shared max age // Validate shared max age
if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge) if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge)
{ {
return false; return false;
} }
else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null) else if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue)
{ {
// Validate max age // Validate max age
if (age > context.ResponseCacheControlHeaderValue.MaxAge) if (age >= context.ResponseCacheControlHeaderValue.MaxAge)
{ {
return false; return false;
} }
else if (context.ResponseCacheControlHeaderValue.MaxAge == null) else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue)
{ {
// Validate expiration // Validate expiration
if (context.ResponseTime > context.TypedResponseHeaders.Expires) if (context.ResponseTime >= context.ResponseExpires)
{ {
return false; return false;
} }
@ -145,21 +145,21 @@ namespace Microsoft.AspNetCore.ResponseCaching
var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl;
// Add min-fresh requirements // Add min-fresh requirements
if (context.RequestCacheControlHeaderValue.MinFresh != null) if (context.RequestCacheControlHeaderValue.MinFresh.HasValue)
{ {
age += context.RequestCacheControlHeaderValue.MinFresh.Value; age += context.RequestCacheControlHeaderValue.MinFresh.Value;
} }
// Validate shared max age, this overrides any max age settings for shared caches // 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 // shared max age implies must revalidate
return false; return false;
} }
else if (cachedControlHeaders.SharedMaxAge == null) else if (!cachedControlHeaders.SharedMaxAge.HasValue)
{ {
// Validate max age // Validate max age
if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge) if (age >= cachedControlHeaders.MaxAge || age >= context.RequestCacheControlHeaderValue.MaxAge)
{ {
// Must revalidate // Must revalidate
if (cachedControlHeaders.MustRevalidate) if (cachedControlHeaders.MustRevalidate)
@ -175,10 +175,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
return false; return false;
} }
else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null) else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue)
{ {
// Validate expiration // Validate expiration
if (context.ResponseTime > context.CachedResponseHeaders.Expires) if (context.ResponseTime >= context.CachedResponseHeaders.Expires)
{ {
return false; return false;
} }

View File

@ -290,7 +290,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void IsResponseCacheable_PastExpiry_NotAllowed() public void IsResponseCacheable_AtExpiry_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Expires = utcNow;
context.TypedResponseHeaders.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
context.ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = utcNow;
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
} }
@ -338,7 +338,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Expires = utcNow;
context.TypedResponseHeaders.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
} }
@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
SharedMaxAge = TimeSpan.FromSeconds(5) SharedMaxAge = TimeSpan.FromSeconds(5)
}; };
context.TypedResponseHeaders.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); context.ResponseTime = utcNow + TimeSpan.FromSeconds(5);
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
} }
@ -408,17 +408,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void IsCachedEntryFresh_PastExpiry_IsNotFresh() public void IsCachedEntryFresh_AtExpiry_IsNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = utcNow;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}, },
Expires = DateTimeOffset.UtcNow Expires = utcNow
}; };
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
@ -449,7 +450,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.CachedEntryAge = TimeSpan.FromSeconds(11); context.CachedEntryAge = TimeSpan.FromSeconds(10);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -490,7 +491,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(5);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -512,7 +513,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MinFresh = TimeSpan.FromSeconds(3) MinFresh = TimeSpan.FromSeconds(2)
}; };
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -542,7 +543,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
MaxAge = TimeSpan.FromSeconds(10), MaxAge = TimeSpan.FromSeconds(10),
} }
}; };
context.CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(5);
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
} }
@ -569,6 +570,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); 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] [Fact]
public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
{ {
@ -591,27 +614,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); 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));
}
} }
} }