Support conditional requests and send 304 when possible
This commit is contained in:
parent
a5e9215982
commit
52f219b16e
|
|
@ -443,41 +443,50 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true))
|
if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true))
|
||||||
{
|
{
|
||||||
var response = _httpContext.Response;
|
// Check conditional request rules
|
||||||
// Copy the cached status code and response headers
|
if (ConditionalRequestSatisfied(cachedResponseHeaders))
|
||||||
response.StatusCode = cachedResponse.StatusCode;
|
|
||||||
foreach (var header in cachedResponse.Headers)
|
|
||||||
{
|
{
|
||||||
response.Headers.Add(header);
|
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
||||||
}
|
|
||||||
|
|
||||||
response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
if (_responseType == ResponseType.HeadersOnly)
|
|
||||||
{
|
|
||||||
responseServed = true;
|
|
||||||
}
|
|
||||||
else if (_responseType == ResponseType.FullReponse)
|
|
||||||
{
|
|
||||||
// Copy the cached response body
|
|
||||||
var body = cachedResponse.Body;
|
|
||||||
|
|
||||||
// Add a content-length if required
|
|
||||||
if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
|
|
||||||
{
|
|
||||||
response.ContentLength = body.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (body.Length > 0)
|
|
||||||
{
|
|
||||||
await response.Body.WriteAsync(body, 0, body.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseServed = true;
|
responseServed = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized.");
|
var response = _httpContext.Response;
|
||||||
|
// Copy the cached status code and response headers
|
||||||
|
response.StatusCode = cachedResponse.StatusCode;
|
||||||
|
foreach (var header in cachedResponse.Headers)
|
||||||
|
{
|
||||||
|
response.Headers.Add(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
if (_responseType == ResponseType.HeadersOnly)
|
||||||
|
{
|
||||||
|
responseServed = true;
|
||||||
|
}
|
||||||
|
else if (_responseType == ResponseType.FullReponse)
|
||||||
|
{
|
||||||
|
// Copy the cached response body
|
||||||
|
var body = cachedResponse.Body;
|
||||||
|
|
||||||
|
// Add a content-length if required
|
||||||
|
if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
|
||||||
|
{
|
||||||
|
response.ContentLength = body.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.Length > 0)
|
||||||
|
{
|
||||||
|
await response.Body.WriteAsync(body, 0, body.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseServed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -489,13 +498,42 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
if (!responseServed && RequestCacheControl.OnlyIfCached)
|
if (!responseServed && RequestCacheControl.OnlyIfCached)
|
||||||
{
|
{
|
||||||
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
||||||
|
|
||||||
responseServed = true;
|
responseServed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseServed;
|
return responseServed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
|
||||||
|
{
|
||||||
|
var ifNoneMatchHeader = RequestHeaders.IfNoneMatch;
|
||||||
|
|
||||||
|
if (ifNoneMatchHeader != null)
|
||||||
|
{
|
||||||
|
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedResponseHeaders.ETag != null)
|
||||||
|
{
|
||||||
|
foreach (var tag in ifNoneMatchHeader)
|
||||||
|
{
|
||||||
|
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= RequestHeaders.IfUnmodifiedSince)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
internal void FinalizeCachingHeaders()
|
internal void FinalizeCachingHeaders()
|
||||||
{
|
{
|
||||||
if (CacheResponse)
|
if (CacheResponse)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -692,6 +693,140 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false));
|
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
|
||||||
|
{
|
||||||
|
var context = CreateTestContext(new DefaultHttpContext());
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
|
||||||
|
// Verify modifications at present succeeds
|
||||||
|
cachedHeaders.Date = utcNow;
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
|
||||||
|
// Verify modifications in the future fails
|
||||||
|
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
|
||||||
|
// Verify modifications at present
|
||||||
|
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
cachedHeaders.LastModified = utcNow;
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
|
||||||
|
// Verify modifications in the future fails
|
||||||
|
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
// This would fail the IfUnmodifiedSince checks
|
||||||
|
requestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
// This would pass the IfUnmodifiedSince checks
|
||||||
|
requestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes()
|
||||||
|
{
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes()
|
||||||
|
{
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
|
{
|
||||||
|
ETag = new EntityTagHeaderValue("\"E1\"")
|
||||||
|
};
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails()
|
||||||
|
{
|
||||||
|
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
|
{
|
||||||
|
ETag = new EntityTagHeaderValue("\"E2\"")
|
||||||
|
};
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var context = CreateTestContext(httpContext);
|
||||||
|
|
||||||
|
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(HttpContext httpContext)
|
private static ResponseCachingContext CreateTestContext(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
return CreateTestContext(
|
return CreateTestContext(
|
||||||
|
|
|
||||||
|
|
@ -636,6 +636,122 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void Serves304_IfIfModifiedSince_Satisfied()
|
||||||
|
{
|
||||||
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
|
{
|
||||||
|
var uniqueId = Guid.NewGuid().ToString();
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
headers.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
headers.Date = DateTimeOffset.UtcNow;
|
||||||
|
headers.Headers["X-Value"] = uniqueId;
|
||||||
|
await context.Response.WriteAsync(uniqueId);
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue;
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
initialResponse.EnsureSuccessStatusCode();
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied()
|
||||||
|
{
|
||||||
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
|
{
|
||||||
|
var uniqueId = Guid.NewGuid().ToString();
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
headers.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
headers.Date = DateTimeOffset.UtcNow;
|
||||||
|
headers.Headers["X-Value"] = uniqueId;
|
||||||
|
await context.Response.WriteAsync(uniqueId);
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void Serves304_IfIfNoneMatch_Satisfied()
|
||||||
|
{
|
||||||
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
|
{
|
||||||
|
var uniqueId = Guid.NewGuid().ToString();
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
headers.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
headers.Date = DateTimeOffset.UtcNow;
|
||||||
|
headers.Headers["X-Value"] = uniqueId;
|
||||||
|
headers.ETag = new EntityTagHeaderValue("\"E1\"");
|
||||||
|
await context.Response.WriteAsync(uniqueId);
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\""));
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
initialResponse.EnsureSuccessStatusCode();
|
||||||
|
Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied()
|
||||||
|
{
|
||||||
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
|
{
|
||||||
|
var uniqueId = Guid.NewGuid().ToString();
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
headers.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
headers.Date = DateTimeOffset.UtcNow;
|
||||||
|
headers.Headers["X-Value"] = uniqueId;
|
||||||
|
headers.ETag = new EntityTagHeaderValue("\"E1\"");
|
||||||
|
await context.Response.WriteAsync(uniqueId);
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\""));
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) =>
|
private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) =>
|
||||||
CreateBuilderWithResponseCaching(app => { }, requestDelegate);
|
CreateBuilderWithResponseCaching(app => { }, requestDelegate);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue