Support conditional requests and send 304 when possible

This commit is contained in:
John Luo 2016-08-29 12:55:38 -07:00
parent a5e9215982
commit 52f219b16e
3 changed files with 320 additions and 31 deletions

View File

@ -443,41 +443,50 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true))
{
var response = _httpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = cachedResponse.StatusCode;
foreach (var header in cachedResponse.Headers)
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{
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);
}
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
responseServed = true;
}
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
@ -489,13 +498,42 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (!responseServed && RequestCacheControl.OnlyIfCached)
{
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
responseServed = true;
}
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()
{
if (CacheResponse)

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
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));
}
[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)
{
return CreateTestContext(

View File

@ -636,6 +636,122 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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) =>
CreateBuilderWithResponseCaching(app => { }, requestDelegate);