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))
|
||||
{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue