Skip to content
This repository was archived by the owner on Nov 22, 2018. It is now read-only.

Conditional requests #22

Merged
merged 1 commit into from
Aug 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 64 additions & 26 deletions src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,41 +443,50 @@ internal async Task<bool> TryServeFromCacheAsync()

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)
{
response.Headers.Add(header);
}

response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);

if (_responseType == ResponseType.HeadersOnly)
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
responseServed = true;
}
else if (_responseType == ResponseType.FullReponse)
else
{
// 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]))
var response = _httpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = cachedResponse.StatusCode;
foreach (var header in cachedResponse.Headers)
{
response.ContentLength = body.Length;
response.Headers.Add(header);
}

if (body.Length > 0)
response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);

if (_responseType == ResponseType.HeadersOnly)
{
await response.Body.WriteAsync(body, 0, body.Length);
responseServed = true;
}
else if (_responseType == ResponseType.FullReponse)
{
// Copy the cached response body
var body = cachedResponse.Body;

responseServed = true;
}
else
{
throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized.");
// 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
Expand All @@ -489,13 +498,42 @@ internal async Task<bool> TryServeFromCacheAsync()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -692,6 +693,140 @@ public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,122 @@ private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initi
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);

Expand Down