-
Notifications
You must be signed in to change notification settings - Fork 523
Handle response content length mismatches (#175) #1155
Conversation
9c4f978
to
83a49d0
Compare
var responseHeaders = FrameResponseHeaders; | ||
if (responseHeaders != null && responseHeaders.HasContentLength) | ||
{ | ||
_responseContentLength = int.Parse(responseHeaders.HeaderContentLength.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.
TryParse, culture, format
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'll leave it as Parse
since that will throw if the app does something like httpContext.Response.Headers["Content-Length"] = "foo";
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.
In that case you should throw from the header setter ideally.
I think we should make sure we have the same logic we use in HttpAbstractions. We should probably look into sharing this with HttpAbsractions somehow.
It's also a good opportunity to make sure we use the same logic for parsing the request header content-length. Right now, it seems like we're probably more lax than we should be.
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.
Oh. And I see you changed this to a long 👍 Were there not any tests failing when this was int.Parse?
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.
No test failures :( It was bound to blow up somewhere.
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.
Wow. I thought our LargeDownload test wrote over 2GB like the LargeUpload test. It looks like the LargeDownload test only has a 1MB body.
We should change the LargeDownload test to be like the LargeUpload test where we check every byte value in a few megabyte scenario, but still ensure that sending over 2GB works.
|
||
protected void TryInitializeResponseContentLength() | ||
{ | ||
if (!_responseContentLength.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.
If there's no content-length header this will re-run for every write. You need a flag to only run it once, or is HasContentLength really that fast?
|
||
if (_responseContentLength.HasValue && _responseBytesWritten < _responseContentLength.Value) | ||
{ | ||
ThrowContentLengthMismatchException(); |
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 a different message "Too few bytes written".
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.
Why even throw at this point? Why not just directly invoke ReportApplicationError?
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.
Why have two different messages when we can have a single one that reports the error in the same way?
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.
because they are different conditions, and the message should be clear which is which.
protected void ThrowContentLengthMismatchException() | ||
{ | ||
_keepAlive = false; | ||
throw new InvalidOperationException($"Response content length mismatch: wrote {_responseBytesWritten} bytes to {_responseContentLength.Value}-byte 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.
Too many bytes written
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, Stream allows InvalidOperationException.
https://msdn.microsoft.com/en-us/library/hh137799(v=vs.110).aspx
@@ -548,6 +551,7 @@ public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationTo | |||
} | |||
|
|||
_responseBytesWritten += data.Count; | |||
VerifyWrite(); |
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 calling pattern is misleading, _responseBytesWritten gets incremented regardless of if you've written the bytes out. Invert it:
VerifyWrite(data.Count);
_responseBytesWritten += data.Count;
and/or move _responseBytesWritten += data.Count;
inside of VerifyWrite and make it VerifyAndUpdateWrite
@Tratcher I've addressed your comments. |
var responseHeaders = FrameResponseHeaders; | ||
if (responseHeaders != null && responseHeaders.HasContentLength) | ||
{ | ||
_responseContentLength = long.Parse(responseHeaders.HeaderContentLength.ToString(), NumberStyles.None); |
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 will result in a FormatException being thrown from Write. Use TryParse and ignore values you can't parse.
When determining if you should chunk, do you check for the presence of a content-length or if you understand it?
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 check for its presence but we don't try to parse it.
I disagree with you on this point. We should throw if the Content-Length
header is set to an invalid value. The whole point of this change is to track errors related to response Content-Length
, this would be one of them. Without a valid value, we can't track it.
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.
Even if you want to throw, you can't throw a FormatException from Stream.Write. It needs to be wrapped in an InvalidOperationException
Updated. |
Added validation to header setter. Not sure I covered all bases though. I'm thinking of adding a |
@@ -201,6 +202,7 @@ public StringValues HeaderContentLength | |||
} | |||
set | |||
{ | |||
ValidateLongValue(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.
can you cache the parsed value internally rather than re-parse it later?
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.
Yeah, it's what I was referring to in my comment above.
@@ -232,6 +233,11 @@ public static void ValidateHeaderCharacters(string headerCharacters) | |||
} | |||
} | |||
|
|||
public static void ValidateLongValue(StringValues value) | |||
{ | |||
long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture); |
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.
Might make things a little easier to debug if this was wrapped in an InvalidOperationException stating the Content-Length header must be a positive decimal number or something like this.
Updated. Caching Content-Length and wrapping |
1aa76e4
to
a837285
Compare
@@ -12,6 +13,7 @@ public partial class FrameRequestHeaders | |||
|
|||
private long _bits = 0; | |||
private HeaderReferences _headers; | |||
private 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.
Does this get cleared if I call Headers.Remove("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.
Good catch. We should add a test case for this.
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.
Done a7e7044
a837285
to
a7e7044
Compare
I'm applying the |
"GET / HTTP/1.1", | ||
"", | ||
""); | ||
await connection.ReceiveEnd( |
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 this result in a RST? or just a FIN?
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.
FIN
|
||
using (var server = new TestServer(httpContext => | ||
{ | ||
httpContext.Response.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.
What if you added a version of this test that called httpContext.Respose.Body.Flush()
here? Same result I hope.
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.
Same result. I've modified this test to test with and without flushing.
@@ -349,7 +361,8 @@ protected override bool RemoveFast(string key) | |||
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase)) | |||
{{ | |||
if ({header.TestBit()}) | |||
{{ | |||
{{{If(header.Identifier == "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.
Missed loop.ClassName == "FrameResponseHeaders" &&
here.
@@ -369,6 +382,7 @@ protected override void ClearFast() | |||
{{ | |||
_bits = 0; | |||
_headers = default(HeaderReferences); | |||
_contentLength = null; |
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.
Missed If here too.
@@ -684,7 +703,7 @@ private void ProduceStart(bool appCompleted) | |||
|
|||
protected Task TryProduceInvalidRequestResponse() | |||
{ | |||
if (_requestRejected) | |||
if (_requestRejected || _applicationException != null) |
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.
Why is this change necessary?
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.
Obviously the application could swallow the exception, so we shouldn't be relying on it. And this change has little to do with invalid requests, right? I guess we're now parsing the request content-length more strictly...
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.
If the response hasn't already started by the time we detect a mismatch in the content length, we can send a 500 response. _applicationException
is set when there's a mismatch, so we need this check to run the error response logic.
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.
That should be handled by the normal call to ProduceEnd()
.
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.
Indeed. My very first change (that I didn't even commit) was calling Abort()
on a content length mismatch. This would prevent the call to ProduceEnd()
in FrameOfT
. But since I moved away from that decision, ProduceEnd()
handles it as expected.
I don't think we need a new event id for this. Treating it as a normal application error is fine with me. |
Updated. |
8e1eb31
to
6412753
Compare
|
{header.SetBit()}; | ||
_headers._{header.Identifier} = value; {(header.EnhancedSetter == false ? "" : $@" | ||
_headers._raw{header.Identifier} = null;")} | ||
}} | ||
}}")} | ||
{(loop.ClassName == "FrameResponseHeaders" ? "public long? HeaderContentLengthValue => _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: I would probably put this in FrameResponseHeaders.cs
@@ -199,6 +204,7 @@ public static string GeneratedFile() | |||
return $@" | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Globalization; |
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: I don't think this is being used.
@@ -304,7 +313,8 @@ protected override void SetValueFast(string key, StringValues value) | |||
case {byLength.Key}: | |||
{{{Each(byLength, header => $@" | |||
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase)) | |||
{{ | |||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "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: $
is unnecessary.
}} | ||
}}{ | ||
If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => $@" | ||
_contentLength = ParseContentLength(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.
No $
needed
@@ -228,12 +235,14 @@ public partial class {loop.ClassName} | |||
return StringValues.Empty; | |||
}} | |||
set | |||
{{ | |||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "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.
No $
.
@@ -349,7 +361,8 @@ protected override bool RemoveFast(string key) | |||
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase)) | |||
{{ | |||
if ({header.TestBit()}) | |||
{{ | |||
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "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.
ditto
@halter73 nits addressed :) |
2e6ffce
to
f8813a6
Compare
#175
InvalidOperationException
might not be the right exception type for this@halter73 @Tratcher @mikeharder @davidfowl