Stricter expiration checks to avoid serving responses when max-age is 0
Cache parsed response headers for performance
This commit is contained in:
parent
c30d471c27
commit
6891d00032
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue