From 9c94a7764bdb1f7194709ec7d5bc011f21f01a37 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Mon, 3 Oct 2016 10:25:06 -0700 Subject: [PATCH 1/2] Improve header parsing performance --- .../CacheControlValues.cs | 21 ++ .../Internal/HttpHeaderParsingHelpers.cs | 118 +++++++++ .../Internal/ResponseCachingContext.cs | 99 ++++--- .../Internal/ResponseCachingPolicyProvider.cs | 90 ++++--- .../ResponseCachingMiddleware.cs | 54 ++-- .../ParsingHelpersTests.cs | 39 +++ .../ResponseCachingMiddlewareTests.cs | 96 ++++--- .../ResponseCachingPolicyProviderTests.cs | 248 ++++++++---------- 8 files changed, 466 insertions(+), 299 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs new file mode 100644 index 0000000..1dd0db8 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs @@ -0,0 +1,21 @@ +// 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.Internal +{ + internal class CacheControlValues + { + public const string MaxAgeString = "max-age"; + public const string MaxStaleString = "max-stale"; + public const string MinFreshString = "min-fresh"; + public const string MustRevalidateString = "must-revalidate"; + public const string NoCacheString = "no-cache"; + public const string NoStoreString = "no-store"; + public const string NoTransformString = "no-transform"; + public const string OnlyIfCachedString = "only-if-cached"; + public const string PrivateString = "private"; + public const string ProxyRevalidateString = "proxy-revalidate"; + public const string PublicString = "public"; + public const string SharedMaxAgeString = "s-maxage"; + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs new file mode 100644 index 0000000..94d4521 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs @@ -0,0 +1,118 @@ +// 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 System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class HttpHeaderParsingHelpers + { + private static readonly string[] DateFormats = new string[] { + // "r", // RFC 1123, required output format but too strict for input + "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) + "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT + "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week + "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone + "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year + "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone + "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year + "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone + + "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 + "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone + "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format + + "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 + "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone + "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week + "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone + }; + + // Try the various date formats in the order listed above. + // We should accept a wide verity of common formats, but only output RFC 1123 style dates. + internal static bool TryParseHeaderDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); + + // Try to get the value of a specific header from a list of headers + // e.g. "header1=10, header2=30" + internal static bool TryParseHeaderTimeSpan(StringValues headers, string headerName, out TimeSpan? value) + { + foreach (var header in headers) + { + var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + index += headerName.Length; + int seconds; + if (!TryParseHeaderInt(index, header, out seconds)) + { + break; + } + value = TimeSpan.FromSeconds(seconds); + return true; + } + } + value = null; + return false; + } + + internal static bool HeaderContains(StringValues headers, string headerName) + { + foreach (var header in headers) + { + var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + return true; + } + } + + return false; + } + + private static bool TryParseHeaderInt(int startIndex, string header, out int value) + { + var found = false; + while (startIndex != header.Length) + { + var c = header[startIndex]; + if (c == '=') + { + found = true; + } + else if (c != ' ') + { + --startIndex; + break; + } + ++startIndex; + } + if (found && startIndex != header.Length) + { + var endIndex = startIndex + 1; + while (endIndex < header.Length) + { + var c = header[endIndex]; + if ((c >= '0') && (c <= '9')) + { + endIndex++; + } + else + { + break; + } + } + var length = endIndex - (startIndex + 1); + if (length > 0) + { + value = int.Parse(header.Substring(startIndex + 1, length), NumberStyles.None, NumberFormatInfo.InvariantInfo); + return true; + } + } + value = 0; + return false; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4fc7292..4d03434 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -5,7 +5,6 @@ using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -13,16 +12,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingContext { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; private DateTimeOffset? _responseDate; private bool _parsedResponseDate; private DateTimeOffset? _responseExpires; private bool _parsedResponseExpires; + private TimeSpan? _responseSharedMaxAge; + private bool _parsedResponseSharedMaxAge; + private TimeSpan? _responseMaxAge; + private bool _parsedResponseMaxAge; internal ResponseCachingContext(HttpContext httpContext, ILogger logger) { @@ -58,85 +55,79 @@ internal ResponseCachingContext(HttpContext httpContext, ILogger logger) internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal ResponseHeaders CachedResponseHeaders { get; set; } + internal IHeaderDictionary CachedResponseHeaders { get; set; } - internal RequestHeaders TypedRequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = HttpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - internal ResponseHeaders TypedResponseHeaders + internal DateTimeOffset? ResponseDate { get { - if (_responseHeaders == null) + if (!_parsedResponseDate) { - _responseHeaders = HttpContext.Response.GetTypedHeaders(); + _parsedResponseDate = true; + DateTimeOffset date; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + { + _responseDate = date; + } + else + { + _responseDate = null; + } } - return _responseHeaders; + return _responseDate; } - } - - internal CacheControlHeaderValue RequestCacheControlHeaderValue - { - get + set { - if (_requestCacheControl == null) - { - _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; + // Don't reparse the response date again if it's explicitly set + _parsedResponseDate = true; + _responseDate = value; } } - internal CacheControlHeaderValue ResponseCacheControlHeaderValue + internal DateTimeOffset? ResponseExpires { get { - if (_responseCacheControl == null) + if (!_parsedResponseExpires) { - _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; + _parsedResponseExpires = true; + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + { + _responseExpires = expires; + } + else + { + _responseExpires = null; + } } - return _responseCacheControl; + return _responseExpires; } } - internal DateTimeOffset? ResponseDate + internal TimeSpan? ResponseSharedMaxAge { get { - if (!_parsedResponseDate) + if (!_parsedResponseSharedMaxAge) { - _parsedResponseDate = true; - _responseDate = TypedResponseHeaders.Date; + _parsedResponseSharedMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); } - return _responseDate; - } - set - { - // Don't reparse the response date again if it's explicitly set - _parsedResponseDate = true; - _responseDate = value; + return _responseSharedMaxAge; } } - internal DateTimeOffset? ResponseExpires + internal TimeSpan? ResponseMaxAge { get { - if (!_parsedResponseExpires) + if (!_parsedResponseMaxAge) { - _parsedResponseExpires = true; - _responseExpires = TypedResponseHeaders.Expires; + _parsedResponseMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); } - return _responseExpires; + return _responseMaxAge; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 0072546..87e3791 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -10,8 +10,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Verify the method @@ -32,7 +30,7 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context) // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (context.RequestCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -42,13 +40,10 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - foreach (var directive in pragmaHeaderValues) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) { - if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) - { - context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); - return false; - } + context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); + return false; } } @@ -57,22 +52,30 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context) public virtual bool IsResponseCacheable(ResponseCachingContext context) { + var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; + // Only cache pages explicitly marked with public - if (!context.ResponseCacheControlHeaderValue.Public) + if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) + { + context.Logger.LogResponseWithNoStoreNotCacheable(); + return false; + } + + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (context.ResponseCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -96,7 +99,7 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) } // Check private - if (context.ResponseCacheControlHeaderValue.Private) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -112,8 +115,8 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) // Check response freshness if (!context.ResponseDate.HasValue) { - if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && - !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && + if (!context.ResponseSharedMaxAge.HasValue && + !context.ResponseMaxAge.HasValue && context.ResponseTime.Value >= context.ResponseExpires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); @@ -125,22 +128,20 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) var age = context.ResponseTime.Value - context.ResponseDate.Value; // Validate shared max age - var sharedMaxAge = context.ResponseCacheControlHeaderValue.SharedMaxAge; - if (age >= sharedMaxAge) + if (age >= context.ResponseSharedMaxAge) { - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!context.ResponseSharedMaxAge.HasValue) { // Validate max age - var maxAge = context.ResponseCacheControlHeaderValue.MaxAge; - if (age >= maxAge) + if (age >= context.ResponseMaxAge) { - context.Logger.LogExpirationMaxAgeExceeded(age, maxAge.Value); + context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); return false; } - else if (!maxAge.HasValue) + else if (!context.ResponseMaxAge.HasValue) { // Validate expiration if (context.ResponseTime.Value >= context.ResponseExpires) @@ -158,44 +159,53 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; + var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements - var minFresh = context.RequestCacheControlHeaderValue.MinFresh; - if (minFresh.HasValue) + TimeSpan? minFresh; + if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); } // Validate shared max age, this overrides any max age settings for shared caches - var sharedMaxAge = cachedControlHeaders.SharedMaxAge; - if (age >= sharedMaxAge) + TimeSpan? cachedSharedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + + if (age >= cachedSharedMaxAge) { // shared max age implies must revalidate - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!cachedSharedMaxAge.HasValue) { - var cachedMaxAge = cachedControlHeaders.MaxAge; - var requestMaxAge = context.RequestCacheControlHeaderValue.MaxAge; + TimeSpan? requestMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + + TimeSpan? cachedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (cachedControlHeaders.MustRevalidate) + if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } + TimeSpan? requestMaxStale; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + // Request allows stale values - var maxStaleLimit = context.RequestCacheControlHeaderValue.MaxStaleLimit; - if (maxStaleLimit.HasValue && age - lowestMaxAge < maxStaleLimit) + if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) { - context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, maxStaleLimit.Value); + context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); return true; } @@ -205,11 +215,11 @@ public virtual bool IsCachedEntryFresh(ResponseCachingContext context) else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) { // Validate expiration - var responseTime = context.ResponseTime.Value; - var expires = context.CachedResponseHeaders.Expires; - if (responseTime >= expires) + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + context.ResponseTime.Value >= expires) { - context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value); + context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); return false; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index dc01df4..a87f2de 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -119,7 +119,7 @@ internal async Task TryServeCachedResponseAsync(ResponseCachingContext con } context.CachedResponse = cachedResponse; - context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + context.CachedResponseHeaders = cachedResponse.Headers; context.ResponseTime = _options.SystemClock.UtcNow; var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; @@ -198,7 +198,7 @@ internal async Task TryServeFromCacheAsync(ResponseCachingContext context) } } - if (context.RequestCacheControlHeaderValue.OnlyIfCached) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -219,8 +219,8 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) var response = context.HttpContext.Response; var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); var varyQueryKeys = new StringValues(context.HttpContext.Features.Get()?.VaryByQueryKeys); - context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? - context.ResponseCacheControlHeaderValue.MaxAge ?? + context.CachedResponseValidFor = context.ResponseSharedMaxAge ?? + context.ResponseMaxAge ?? (context.ResponseExpires - context.ResponseTime.Value) ?? DefaultExpirationTimeSpan; @@ -256,7 +256,7 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) { context.ResponseDate = context.ResponseTime.Value; // Setting the date on the raw response headers. - context.TypedResponseHeaders.Date = context.ResponseDate; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value); } // Store the response on the state @@ -266,7 +266,7 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) StatusCode = context.HttpContext.Response.StatusCode }; - foreach (var header in context.TypedResponseHeaders.Headers) + foreach (var header in context.HttpContext.Response.Headers) { if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) { @@ -282,7 +282,7 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { - var contentLength = context.TypedResponseHeaders.ContentLength; + var contentLength = context.HttpContext.Response.ContentLength; if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) { var bufferStream = context.ResponseCachingStream.GetBufferStream(); @@ -355,37 +355,51 @@ internal static void UnshimResponseStream(ResponseCachingContext context) internal static bool ContentIsNotModified(ResponseCachingContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; - var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; + var ifNoneMatchHeader = context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch]; - if (ifNoneMatchHeader != null) + if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (cachedResponseHeaders.ETag != null) + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) { - foreach (var tag in ifNoneMatchHeader) + EntityTagHeaderValue eTag; + if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) + foreach (var tag in ifNoneMatchHeader) { - context.Logger.LogNotModifiedIfNoneMatchMatched(tag); - return true; + EntityTagHeaderValue requestETag; + if (EntityTagHeaderValue.TryParse(tag, out requestETag) && + eTag.Compare(requestETag, useStrongComparison: false)) + { + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; + } } } } } else { - var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; - if (ifUnmodifiedSince != null) + var ifUnmodifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince]; + if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { - var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; - if (lastModified <= ifUnmodifiedSince) + DateTimeOffset modified; + if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { - context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(lastModified.Value, ifUnmodifiedSince.Value); + return false; + } + + DateTimeOffset unmodifiedSince; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + modified <= unmodifiedSince) + { + context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); return true; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs new file mode 100644 index 0000000..fb495bb --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs @@ -0,0 +1,39 @@ +// 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.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ParsingHelpersTests + { + [Theory] + [InlineData("h=1", "h", 1)] + [InlineData("header1=3, header2=10", "header1", 3)] + [InlineData("header1 =45, header2=80", "header1", 45)] + [InlineData("header1= 89 , header2=22", "header1", 89)] + [InlineData("header1= 89 , header2= 42", "header2", 42)] + void TryGetHeaderValue_Succeeds(string headerValue, string headerName, int expectedValue) + { + TimeSpan? value; + Assert.True(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); + Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + } + + [Theory] + [InlineData("h=", "h")] + [InlineData("header1=, header2=10", "header1")] + [InlineData("header1 , header2=80", "header1")] + [InlineData("h=10", "header")] + [InlineData("", "")] + [InlineData(null, null)] + void TryGetHeaderValue_Fails(string headerValue, string headerName) + { + TimeSpan? value; + Assert.False(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index fb74241..ad607e9 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -23,10 +23,10 @@ public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { OnlyIfCached = true - }; + }.ToString(); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); @@ -149,7 +149,7 @@ public void ContentIsNotModified_NotConditionalRequest_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -161,22 +161,22 @@ public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader() var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present succeeds - context.CachedResponseHeaders.Date = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -192,25 +192,25 @@ public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHead var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -226,13 +226,13 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue( var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would fail the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, @@ -245,13 +245,13 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would pass the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -261,9 +261,9 @@ public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -289,12 +289,10 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHe { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = responseETag - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( @@ -307,12 +305,10 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E2\"") - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -340,10 +336,10 @@ public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCac var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(context.ShouldCacheResponse); @@ -375,7 +371,7 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -389,12 +385,12 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -408,13 +404,13 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailab var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12), SharedMaxAge = TimeSpan.FromSeconds(13) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -538,11 +534,11 @@ public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - Assert.Null(context.TypedResponseHeaders.Date); + Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date])); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } @@ -553,14 +549,14 @@ public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs index 36fbb6a..02a068e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs @@ -88,10 +88,10 @@ public void IsRequestCacheable_NoCache_NotAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( @@ -105,10 +105,10 @@ public void IsRequestCacheable_NoStore_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); @@ -158,10 +158,10 @@ public void IsResponseCacheable_Public_Allowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -172,11 +172,11 @@ public void IsResponseCacheable_NoCache_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -189,14 +189,14 @@ public void IsResponseCacheable_RequestNoStore_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -209,11 +209,11 @@ public void IsResponseCacheable_ResponseNoStore_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoStore = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -226,10 +226,10 @@ public void IsResponseCacheable_SetCookieHeader_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -243,10 +243,10 @@ public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -260,11 +260,11 @@ public void IsResponseCacheable_Private_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, Private = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -279,10 +279,10 @@ public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -342,10 +342,10 @@ public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -359,13 +359,13 @@ public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -378,14 +378,14 @@ public void IsResponseCacheable_AtExpiry_NotAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Expires = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -401,13 +401,13 @@ public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -421,13 +421,13 @@ public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -443,13 +443,13 @@ public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -463,13 +463,13 @@ public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -486,7 +486,7 @@ public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -500,13 +500,11 @@ public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - } - }; + Public = true + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -520,14 +518,12 @@ public void IsCachedEntryFresh_AtExpiry_IsNotFresh() var context = TestUtils.CreateTestContext(sink); context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - }, - Expires = utcNow - }; + Public = true + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -543,15 +539,13 @@ public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -565,15 +559,13 @@ public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -589,16 +581,14 @@ public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(15) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -612,16 +602,14 @@ public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -634,18 +622,16 @@ public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - } - }; + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(3); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -660,17 +646,15 @@ public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - } - }; + MaxAge = TimeSpan.FromSeconds(10), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -684,19 +668,17 @@ public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -710,19 +692,17 @@ public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(1) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -736,20 +716,18 @@ public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MustRevalidate = true - } - }; + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); From e01431f33c07151c2129f4d92e2b7ceebbb8653a Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 9 Dec 2016 14:55:53 -0800 Subject: [PATCH 2/2] Relocate improvements to HttpAbstractions --- .../CacheControlValues.cs | 21 ---- .../Internal/HttpHeaderParsingHelpers.cs | 118 ------------------ .../Internal/MemoryResponseCache.cs | 2 +- .../Internal/ResponseCachingContext.cs | 8 +- .../Internal/ResponseCachingPolicyProvider.cs | 35 +++--- .../ResponseCachingMiddleware.cs | 32 +++-- .../ParsingHelpersTests.cs | 39 ------ .../ResponseCachingMiddlewareTests.cs | 27 ++-- 8 files changed, 54 insertions(+), 228 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs deleted file mode 100644 index 1dd0db8..0000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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.Internal -{ - internal class CacheControlValues - { - public const string MaxAgeString = "max-age"; - public const string MaxStaleString = "max-stale"; - public const string MinFreshString = "min-fresh"; - public const string MustRevalidateString = "must-revalidate"; - public const string NoCacheString = "no-cache"; - public const string NoStoreString = "no-store"; - public const string NoTransformString = "no-transform"; - public const string OnlyIfCachedString = "only-if-cached"; - public const string PrivateString = "private"; - public const string ProxyRevalidateString = "proxy-revalidate"; - public const string PublicString = "public"; - public const string SharedMaxAgeString = "s-maxage"; - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs deleted file mode 100644 index 94d4521..0000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs +++ /dev/null @@ -1,118 +0,0 @@ -// 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 System.Globalization; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal static class HttpHeaderParsingHelpers - { - private static readonly string[] DateFormats = new string[] { - // "r", // RFC 1123, required output format but too strict for input - "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) - "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT - "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week - "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone - "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year - "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone - "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year - "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone - - "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 - "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone - "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format - - "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 - "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone - "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week - "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone - }; - - // Try the various date formats in the order listed above. - // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - internal static bool TryParseHeaderDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); - - // Try to get the value of a specific header from a list of headers - // e.g. "header1=10, header2=30" - internal static bool TryParseHeaderTimeSpan(StringValues headers, string headerName, out TimeSpan? value) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - index += headerName.Length; - int seconds; - if (!TryParseHeaderInt(index, header, out seconds)) - { - break; - } - value = TimeSpan.FromSeconds(seconds); - return true; - } - } - value = null; - return false; - } - - internal static bool HeaderContains(StringValues headers, string headerName) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - return true; - } - } - - return false; - } - - private static bool TryParseHeaderInt(int startIndex, string header, out int value) - { - var found = false; - while (startIndex != header.Length) - { - var c = header[startIndex]; - if (c == '=') - { - found = true; - } - else if (c != ' ') - { - --startIndex; - break; - } - ++startIndex; - } - if (found && startIndex != header.Length) - { - var endIndex = startIndex + 1; - while (endIndex < header.Length) - { - var c = header[endIndex]; - if ((c >= '0') && (c <= '9')) - { - endIndex++; - } - else - { - break; - } - } - var length = endIndex - (startIndex + 1); - if (length > 0) - { - value = int.Parse(header.Substring(startIndex + 1, length), NumberStyles.None, NumberFormatInfo.InvariantInfo); - return true; - } - } - value = 0; - return false; - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 4a855e2..f2509c8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -24,7 +24,7 @@ public MemoryResponseCache(IMemoryCache cache) public Task GetAsync(string key) { var entry = _cache.Get(key); - + var memoryCachedResponse = entry as MemoryCachedResponse; if (memoryCachedResponse != null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4d03434..eeed0d9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -65,7 +65,7 @@ internal DateTimeOffset? ResponseDate { _parsedResponseDate = true; DateTimeOffset date; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) { _responseDate = date; } @@ -92,7 +92,7 @@ internal DateTimeOffset? ResponseExpires { _parsedResponseExpires = true; DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) { _responseExpires = expires; } @@ -112,7 +112,7 @@ internal TimeSpan? ResponseSharedMaxAge if (!_parsedResponseSharedMaxAge) { _parsedResponseSharedMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); } return _responseSharedMaxAge; } @@ -125,7 +125,7 @@ internal TimeSpan? ResponseMaxAge if (!_parsedResponseMaxAge) { _parsedResponseMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); } return _responseMaxAge; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 87e3791..c37fcac 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -30,7 +30,7 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context) // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -40,7 +40,7 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); return false; @@ -55,27 +55,22 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; // Only cache pages explicitly marked with public - if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) + if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) - { - context.Logger.LogResponseWithNoStoreNotCacheable(); - return false; - } - - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString) + || HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -99,7 +94,7 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) } // Check private - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -159,12 +154,12 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context) public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements TimeSpan? minFresh; - if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) + if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); @@ -172,7 +167,7 @@ public virtual bool IsCachedEntryFresh(ResponseCachingContext context) // Validate shared max age, this overrides any max age settings for shared caches TimeSpan? cachedSharedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); if (age >= cachedSharedMaxAge) { @@ -183,24 +178,24 @@ public virtual bool IsCachedEntryFresh(ResponseCachingContext context) else if (!cachedSharedMaxAge.HasValue) { TimeSpan? requestMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); TimeSpan? cachedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) + if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } TimeSpan? requestMaxStale; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); // Request allows stale values if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) @@ -216,7 +211,7 @@ public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { // Validate expiration DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && context.ResponseTime.Value >= expires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index a87f2de..a15353a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,11 +2,11 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -198,7 +198,7 @@ internal async Task TryServeFromCacheAsync(ResponseCachingContext context) } } - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -359,26 +359,24 @@ internal static bool ContentIsNotModified(ResponseCachingContext context) if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) + if (ifNoneMatchHeader.Count == 1 && string.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) + EntityTagHeaderValue eTag; + IList ifNoneMatchEtags; + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) + && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { - EntityTagHeaderValue eTag; - if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) + foreach (var requestETag in ifNoneMatchEtags) { - foreach (var tag in ifNoneMatchHeader) + if (eTag.Compare(requestETag, useStrongComparison: false)) { - EntityTagHeaderValue requestETag; - if (EntityTagHeaderValue.TryParse(tag, out requestETag) && - eTag.Compare(requestETag, useStrongComparison: false)) - { - context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); - return true; - } + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; } } } @@ -389,14 +387,14 @@ internal static bool ContentIsNotModified(ResponseCachingContext context) if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { DateTimeOffset modified; - if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && - !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { return false; } DateTimeOffset unmodifiedSince; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + if (HeaderUtilities.TryParseDate(ifUnmodifiedSince, out unmodifiedSince) && modified <= unmodifiedSince) { context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs deleted file mode 100644 index fb495bb..0000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.ResponseCaching.Internal; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ParsingHelpersTests - { - [Theory] - [InlineData("h=1", "h", 1)] - [InlineData("header1=3, header2=10", "header1", 3)] - [InlineData("header1 =45, header2=80", "header1", 45)] - [InlineData("header1= 89 , header2=22", "header1", 89)] - [InlineData("header1= 89 , header2= 42", "header2", 42)] - void TryGetHeaderValue_Succeeds(string headerValue, string headerName, int expectedValue) - { - TimeSpan? value; - Assert.True(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); - } - - [Theory] - [InlineData("h=", "h")] - [InlineData("header1=, header2=10", "header1")] - [InlineData("header1 , header2=80", "header1")] - [InlineData("h=10", "header")] - [InlineData("", "")] - [InlineData(null, null)] - void TryGetHeaderValue_Fails(string headerValue, string headerName) - { - TimeSpan? value; - Assert.False(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index ad607e9..bced86f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; @@ -251,7 +250,7 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -262,8 +261,7 @@ public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -291,7 +289,6 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHe var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); @@ -306,14 +303,28 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } + [Fact] + public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; + + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); + } + [Fact] public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() {