Header parsing#70
Conversation
| _requestHeaders = HttpContext.Request.GetTypedHeaders(); | ||
| _parsedResponseDate = true; | ||
| DateTimeOffset date; | ||
| if (DateTimeOffset.TryParse(HttpContext.Response.Headers[HeaderNames.Date], out date)) |
| @@ -0,0 +1,23 @@ | |||
| using System; | |||
| @@ -0,0 +1,74 @@ | |||
| using System; | |||
| { | ||
| internal class CacheControlValues | ||
| { | ||
| public const string MaxAgeString = "max-age"; |
There was a problem hiding this comment.
Any chance we make these strings public in HttpAbstractions so we don't duplicated it here?
| _responseDate = TypedResponseHeaders.Date; | ||
| _parsedResponseSharedMaxAge = true; | ||
| _responseSharedMaxAge = null; | ||
| foreach (var header in HttpContext.Response.Headers[HeaderNames.CacheControl]) |
There was a problem hiding this comment.
Weren't we trying to eliminate the use of foreach to reduce allocations for enumerators?
| _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; | ||
| _parsedResponseExpires = true; | ||
| DateTimeOffset expires; | ||
| if (ParsingHelpers.TryStringToDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) |
There was a problem hiding this comment.
TryStringToDate => TryParseDate
| _responseExpires = TypedResponseHeaders.Expires; | ||
| _parsedResponseMaxAge = true; | ||
| _responseMaxAge = null; | ||
| foreach (var header in HttpContext.Response.Headers[HeaderNames.CacheControl]) |
There was a problem hiding this comment.
Code looks very similar to parsing ResponseSharedMaxAge, extract into a helper method in ParsingHelpers?
| var expires = context.CachedResponseHeaders.Expires; | ||
| if (responseTime >= expires) | ||
| DateTimeOffset expires; | ||
| if (context.CachedResponseHeaders[HeaderNames.Expires].Count > 0) |
There was a problem hiding this comment.
Combine the nested if statements.
| { | ||
| // 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. | ||
| if (DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, |
There was a problem hiding this comment.
so return DateTimeOffset.TryParseExact(...)? Why the if statement? In fact you can do => DateTimeOffset.TryParseExact(...)
| return false; | ||
| } | ||
|
|
||
| internal static bool TryParseHeaderValue(int startIndex, string header, out int value) |
| while (endIndex < header.Length) | ||
| { | ||
| var c = header[endIndex]; | ||
| if ((c >= '0') && (c <= '9')) |
There was a problem hiding this comment.
what about spaces between equals and the first number? max-age = 60?
There was a problem hiding this comment.
I could have swore I tested that... will fix
| context.Logger.LogNotModifiedIfNoneMatchMatched(tag); | ||
| return true; | ||
| EntityTagHeaderValue requestETag; | ||
| if (EntityTagHeaderValue.TryParse(tag, out requestETag)) |
| var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; | ||
| if (lastModified <= ifUnmodifiedSince) | ||
| DateTimeOffset modified; | ||
| if (!ParsingHelpers.TryStringToDate(cachedResponseHeaders[HeaderNames.LastModified], out modified)) |
|
We should probably see whether we want to do the parsing on demand each time or parse all of the cache-control values in one go, need to test the performance of the two approaches. |
| @@ -0,0 +1,35 @@ | |||
| using Microsoft.AspNetCore.ResponseCaching.Internal; | |||
There was a problem hiding this comment.
No copyright = copyleft and we don't like that
| @@ -0,0 +1,100 @@ | |||
| // Copyright (c) .NET Foundation. All rights reserved. | |||
There was a problem hiding this comment.
This file should go in the Internal folder.
| internal static bool TryParseDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, | ||
| DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); | ||
|
|
||
| internal static bool TryGetHeaderValue(StringValues headers, string headerName, out int value) |
There was a problem hiding this comment.
Hmm the method name isn't really descriptive. Maybe TryParseHeaderInt? Should also consolidate the naming so maybe the previous method should be renamed to TryParseHeaderDate
|
|
||
| // Check no-store | ||
| if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) | ||
| foreach (var header in context.HttpContext.Request.Headers[HeaderNames.CacheControl]) |
There was a problem hiding this comment.
This could be extracted in a helper as well. ParsingHelper.HeaderContains(string)
|
|
||
| namespace Microsoft.AspNetCore.ResponseCaching.Internal | ||
| { | ||
| internal static class ParsingHelpers |
There was a problem hiding this comment.
HttpHeaderParsingHelpers?
| DateTimeOffset unmodifiedSince; | ||
| if (ParsingHelpers.TryParseDate(ifUnmodifiedSince, out unmodifiedSince)) | ||
| { | ||
| if (modified <= unmodifiedSince) |
| { | ||
| context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value); | ||
| return false; | ||
| if (context.ResponseTime.Value >= expires) |
| } | ||
|
|
||
| TimeSpan? requestMaxStale = null; | ||
| if (ParsingHelpers.TryGetHeaderValue(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out seconds)) |
There was a problem hiding this comment.
Actually this can be simplified by having a ParsingHelpers.TryParseTimeSpan (or some consistent naming) that returns a TimeSpan.
There was a problem hiding this comment.
Can't pass a nullable as an out parameter :(
e8de495 to
54be93e
Compare
54be93e to
aba398d
Compare
| { | ||
| internal static class HttpHeaderParsingHelpers | ||
| { | ||
| private static readonly string[] DateFormats = new string[] { |
There was a problem hiding this comment.
Is this duplicated from HttpAbstractions?
There was a problem hiding this comment.
It is private in that repo. We are considering moving a few parts of this pr to abstractions so we'll see if this is still needed here.
| if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) | ||
| { | ||
| if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) | ||
| foreach (var tag in ifNoneMatchHeader) |
There was a problem hiding this comment.
I don't think you can use a foreach here since it's not guaranteed that each string contains only one ETag due how StringValues from HTTP headers are stored. Let's take a look at how it is parsed in Abstractions.
| var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; | ||
| if (lastModified <= ifUnmodifiedSince) | ||
| DateTimeOffset modified; | ||
| if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && |
There was a problem hiding this comment.
I have this fixed in my clean up commit but just to note, both sides of the && operator are evaluated regardless of whether they are true or false. See example here: https://gist.github.com/JunTaoLuo/87f8b2e727c956333fa036f01b3275af
There was a problem hiding this comment.
Um, the first one should be TrueStatement to actually test this
There was a problem hiding this comment.
Oops, you're right. And in that case it works as you mentioned. Undid my changes. Must be confusing && with another operator we tested before.
|
|
||
| if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) | ||
| EntityTagHeaderValue eTag; | ||
| if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) |
There was a problem hiding this comment.
The previous comment got collapsed. But this need to be updated to match the parsing behaviour in Abstractions.
|
Requires aspnet/HttpAbstractions#738 |
7322d8f to
3163c85
Compare
b8b6c9e to
313e047
Compare
313e047 to
c9685cc
Compare
|
Perf results on ResponseCachingPlaintextCached: |
|
🐑 🇮🇹 |
c9685cc to
e01431f
Compare
Still a bit of cleanup needed.
Saved about
791bytes per request.1627.6originally836.6afterBenchmarks has shown an average of
3.386%increase in RPS