-
Notifications
You must be signed in to change notification settings - Fork 523
Use ContentLength as primary data source #1313
Conversation
Run through a debug and it does correctly call via the interface to |
434afc9
to
cdcc2c6
Compare
Hows the perf? |
{ | ||
if (ContentLength.HasValue) | ||
{ | ||
return ContentLength.ToString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HeaderUtilities.FormatInt64.
Would it be worth storing the string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed all occurrences.
Don't think its worth storing the string; is another item to keep in sync; check if if exists/generate, clear. Is there a (non-test) scenario where its read multiple times as a string; when the numeric is available?
@@ -266,18 +268,15 @@ protected virtual void OnConsumedBytes(int count) | |||
return new ForChunkedEncoding(keepAlive, headers, context); | |||
} | |||
|
|||
var unparsedContentLength = headers.HeaderContentLength; | |||
if (unparsedContentLength.Count > 0) | |||
if (headers.ContentLength.HasValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there still a 400 produced for an invalid content-length header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is now (was throwing wrong exception, had to split request/response); is thrown during header parsing rather than message body - have added test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not certain its logged in same way though... looking into it
The api change is having the desired effect on the Content-Length Prior
This PR
This PR with HttpAbstractions
However, am getting a perf decrease overall when more headers are added which am looking into |
The test is just for adding headers not for response output 😒 |
{ | ||
if (appCompleted && StatusCode != StatusCodes.Status101SwitchingProtocols) | ||
{ | ||
// Since the app has completed and we are only now generating | ||
// the headers we can safely set the Content-Length to 0. | ||
responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); | ||
responseHeaders.ContentLength = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this be slower than SetRawContentLength?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is an extra 8 byte field in every header set to make the occasional 1 byte write of 0
slightly faster (as most responses will have some content) - can always special case 0 in the copyfrom..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your justification makes sense.
} | ||
|
||
[Theory] | ||
[MemberData(nameof(BadContentLengths))] | ||
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like BadContentLengths contains a negative value. I think we should include that. It would be nice if BadRequestIfContentLengthInvalid also included a test case for negative values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also add a test specifically for setting a negative Content-Length using the new IHeaderDictionary property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
{{{(header.Identifier == "ContentLength" ? $@" | ||
if (ContentLength.HasValue) | ||
{{ | ||
ThrowInvalid{(className == "FrameResponseHeaders" ? "Response" : "Request")}ContentLengthException(AppendValue(HeaderUtilities.FormatInt64(ContentLength.Value), value).ToString()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this meant to guard against more than one Content-Length header? If so, an error message like "Invalid Content-Length: \"{value}\". Value must be a positive integral number." seems misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add extra exception
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does throwing from header parsing get logged ok, or do I have to call logger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing a BadHttpRequestException should be sufficient, since it's supposed to be caught and logged in RequestProcessingAsync.
It looks like SetBadRequestState
no longer logs the error and our tests didn't catch it though... 😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logging is broken when throwing a BadHttpRequestException without using RejectRequest() for cases when the exception can't easily be thrown from inside the Frame class as is the case here.
A situation very similar to the one you're running into now can be found elsewhere in this class.
This commit regressed the logging, but I can't find the PR that changed it. The commit seems to have been pushed sometime between #1163 and #1166.
@CesarBS Can you take a look? We can create an issue if it requires a big change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR? #993
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good find. I wasn't looking through @mikeharder's PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing multiple content lengths instead
[MethodImpl(MethodImplOptions.NoInlining)] | ||
private static byte[] CreateNumericBytesScratch() | ||
{ | ||
return (_numericBytesScratch = new byte[_maxPositiveLongByteLength]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't love assignments inside expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed
{Each(loop.Headers, header => $@" | ||
case {header.Index}: | ||
goto state{header.Index}; | ||
{Each(loop.Headers.Where(header => header.Index >= 0), header => $@" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using Index = -1
to distinguish the Content-Length header seems a little weird. Couldn't you just filter on Name
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
{(loop.ClassName == "FrameResponseHeaders" ? "_contentLength = null;" : "")} | ||
if(FrameHeaders.BitCount(_bits) > 12) | ||
ContentLength = null; | ||
if(FrameHeaders.BitCount(_bits) > 11) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't this just a perf optimization for the case a lot of headers (specifically in _headers
which now wouldn't include Content-Length) were set? Why would require less headers to be set to take this path now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
}).Concat(corsRequestHeaders).Select((header, index) => new KnownHeader | ||
}) | ||
.Concat(corsRequestHeaders) | ||
.Where((header) => header != "Content-Length") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be cleaner just to remove "Content-Length" from commonHeaders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase)); | ||
|
||
public long? ContentLength { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have a custom setter, so we can throw immediately if this is set to a negative value or _isReadOnly is true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With throw an InvalidOp for both Request and Response as will need to be manually set rather than input Request set (so server error 500)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ArgumentOutOfRangeException instead
return (_numericBytesScratch = new byte[_maxPositiveLongByteLength]); | ||
} | ||
|
||
public void CopyFromNumeric(long value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing to ulong
as it doesn't handle negatives; either that or make internal
Added bunch more tests; resolved feedback - other than GeneratedHeaders stuff, want to resolve the perf regression |
56cdc67
to
421d1a1
Compare
Append() before 80 bytes, after 64 bytes zeroed AppendNonPrimaryHeaders() before 888 bytes, after 48 bytes zeroed
Ok... went down this "lost performance" rabbit hole as was setting the Content-Length as string in some of the tests 🤒 Anyway, improves found along the way... |
Looking better Pre
Post
Post + HttpAbstractions
Still need to clean-up KnownHeaders |
Not happy with Output perf; working on it 😢 |
5847bfc
to
04229be
Compare
Fixed the Header output Before
After
|
@@ -43,6 +43,9 @@ internal static BadHttpRequestException GetException(RequestRejectionReason reas | |||
case RequestRejectionReason.MalformedRequestInvalidHeaders: | |||
ex = new BadHttpRequestException("Malformed request: invalid headers.", StatusCodes.Status400BadRequest); | |||
break; | |||
case RequestRejectionReason.MultipleContentLengths: | |||
ex = new BadHttpRequestException("Multiple content length headers.", StatusCodes.Status400BadRequest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use header name Content-Length
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
{ | ||
if (count > 0) | ||
{ | ||
throw new InvalidDataException("Consuming non-existant data"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-existent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -202,6 +202,19 @@ public class BadHttpRequestTests | |||
} | |||
} | |||
|
|||
[Fact] | |||
public async Task BadRequestIfContentLengthInvalid() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test with negative value too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -151,6 +153,86 @@ public void ThrowsWhenClearingHeadersAfterReadOnlyIsSet() | |||
Assert.Throws<InvalidOperationException>(() => dictionary.Clear()); | |||
} | |||
|
|||
[Fact] | |||
public void CorrectContentLengthsOutput() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be in MemoryPoolIteratorTests
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase)); | ||
|
||
public long? ContentLength { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: style, bracket on new line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
@@ -412,11 +423,26 @@ public static unsafe TransferCoding GetFinalTransferCoding(StringValues transfer | |||
return transferEncodingOptions; | |||
} | |||
|
|||
private static void ThrowInvalidContentLengthException(string value) | |||
protected static void ThrowInvalidResponseContentLengthException(long value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These exceptions can move to respective classes (Request/Response)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
throw new ArgumentOutOfRangeException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number."); | ||
} | ||
|
||
protected static void ThrowInvalidResponseContentLengthException(string value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
{ | ||
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number."); | ||
} | ||
|
||
protected static void ThrowInvalidRequestContentLengthException(string value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
0f3b34e
to
dc8004e
Compare
@@ -507,35 +533,46 @@ protected void CopyToFast(ref MemoryPoolIterator output) | |||
}} | |||
}} | |||
|
|||
if({header.TestNotTempBit()}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we know this is actually faster than doing the tempBits &= ~{1L << header.Index}L;
before the if condition and then comparing tempBits
to zero. If so, why are you doing it the old way just for the Content-Length header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes is faster, measured before and after, significant but not vast; missed content-length accidentally because its now a snowflake 😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I completely missed a commit 😱
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
EnhancedSetter = enhancedHeaders.Contains("Content-Length"), | ||
PrimaryHeader = responsePrimaryHeaders.Contains("Content-Length") | ||
}}) | ||
.ToArray(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: We should have done this before, but can we assert that responseHeaders
and requestHeaders
have no more than 64 items and/or the max Index
is 62.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
63 for reponseHeaders as it steals one for Content-Length in CopyTo(ref MemoryPoolIterator output)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
{ | ||
if (bytesLeftInBlock < 1) | ||
{ | ||
goto overflow; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this that much better than just calling CopyFromNumericOverflow(value);
and returning? I prefer avoiding goto if we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not in a way I could probably sensibility justify... It generates the code I'd like the Jit to generate 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
|
Didn't pickup all my local changes in the final commit; fixed; also addressed feedback |
Most of the changes were in; though hadn't picked up the |
throw BadHttpRequestException.GetException(RequestRejectionReason.InvalidContentLength, value); | ||
} | ||
|
||
private static void ThrowMultipleContentLengths() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename to ThrowMultipleContentLengthsException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
React to IHeaderDictionary ContentLength change aspnet/HttpAbstractions#757
/cc @Tratcher