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 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;
}
}
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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));
}
}
}