From 7d716d20072b50ea7c6f9e56cc31ed204de1cd85 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 27 Jul 2016 20:06:14 +0100 Subject: [PATCH] Date & Age handling --- .../IResponseCachingOptions.cs | 10 +++ .../Internal/CachedResponse.cs | 2 + .../ResponseCachingContext.cs | 71 ++++++++++++++++--- 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs new file mode 100644 index 0000000000..4c121c8a63 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.ResponseCaching +{ + interface IResponseCachingOptions + { + int MaxCachedItemBytes { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs index 2c43a562a0..63caf4bcc8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal @@ -12,5 +13,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); internal byte[] Body { get; set; } + public DateTimeOffset Created { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index a493e47e4d..a163e79cb6 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -2,7 +2,9 @@ // 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.IO; +using System.Globalization; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCaching public class ResponseCachingContext { private string _cacheKey; + private RequestType _requestType; public ResponseCachingContext(HttpContext httpContext, IResponseCache cache) { @@ -43,12 +46,24 @@ namespace Microsoft.AspNetCore.ResponseCaching private bool CacheResponse { get; set; } + private bool IsProxied { get; set; } + public bool CheckRequestAllowsCaching() { // Verify the method // TODO: What other methods should be supported? - if (!string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + if (string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) { + _requestType = RequestType.FullReponse; + } + else if (string.Equals("HEAD", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase) || + string.Equals("OPTIONS", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + { + _requestType = RequestType.HeadersOnly; + } + else + { + _requestType = RequestType.NotCached; return false; } @@ -137,14 +152,24 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers[pair.Key] = pair.Value; } - // TODO: Update cache headers (Age) - response.Headers["Served_From_Cache"] = DateTime.Now.ToString(); + // TODO: Allow setting proxied _isProxied + var age = Math.Max((DateTimeOffset.UtcNow - cacheEntry.Created).TotalSeconds, 0.0); + var ageString = (age > int.MaxValue ? int.MaxValue : (int)age).ToString(CultureInfo.InvariantCulture); + response.Headers[IsProxied ? "Age" : "X-Cache-Age"] = ageString; - // Copy the cached response body - var body = cachedResponse.Body; - if (body.Length > 0) + if (_requestType == RequestType.HeadersOnly) { - await response.Body.WriteAsync(body, 0, body.Length); + response.Headers["Content-Length"] = "0"; + } + else + { + // Copy the cached response body + var body = cachedResponse.Body; + response.Headers["Content-Length"] = body.Length.ToString(CultureInfo.InvariantCulture); + if (body.Length > 0) + { + await response.Body.WriteAsync(body, 0, body.Length); + } } return true; } @@ -173,7 +198,8 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void FinalizeCaching() { - if (CacheResponse) + // Don't cache errors? 404 etc + if (CacheResponse && HttpContext.Response.StatusCode == 200) { var response = HttpContext.Response; var varyHeaderValue = response.Headers["Vary"]; @@ -194,12 +220,30 @@ namespace Microsoft.AspNetCore.ResponseCaching // Store the response to cache var cachedResponse = new CachedResponse { + Created = DateTimeOffset.UtcNow, StatusCode = HttpContext.Response.StatusCode, Body = Buffer.ToArray() }; - foreach (var pair in HttpContext.Response.Headers) + + var headers = HttpContext.Response.Headers; + var count = headers.Count + - (headers.ContainsKey("Date") ? 1 : 0) + - (headers.ContainsKey("Content-Length") ? 1 : 0) + - (headers.ContainsKey("Age") ? 1 : 0); + var cachedHeaders = new List>(count); + var age = 0; + foreach (var entry in headers) { - cachedResponse.Headers[pair.Key] = pair.Value; + // Reduce create date by Age + if (entry.Key == "Age" && int.TryParse(entry.Value, out age) && age > 0) + { + cacheEntry.Created -= new TimeSpan(0, 0, age); + } + // Don't copy Date header or Content-Length + else if (entry.Key != "Date" && entry.Key != "Content-Length") + { + cachedHeaders.Add(entry); + } } Cache.Set(_cacheKey, cachedResponse); // TODO: Timeouts @@ -215,5 +259,12 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unhook the response stream. HttpContext.Response.Body = OriginalResponseStream; } + + private enum RequestType + { + NotCached = 0, + HeadersOnly, + FullReponse + } } }