Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Handle response content length mismatches (#175) #1155

Merged
merged 1 commit into from
Oct 11, 2016

Conversation

cesarblum
Copy link
Contributor

@cesarblum cesarblum commented Oct 7, 2016

#175

  • TODO: InvalidOperationException might not be the right exception type for this
  • TODO: is logging this as application errors OK or should we add new event IDs for response length mismatches?

@halter73 @Tratcher @mikeharder @davidfowl

@cesarblum cesarblum force-pushed the cesarbs/track-response-content-length branch from 9c4f978 to 83a49d0 Compare October 7, 2016 17:25
var responseHeaders = FrameResponseHeaders;
if (responseHeaders != null && responseHeaders.HasContentLength)
{
_responseContentLength = int.Parse(responseHeaders.HeaderContentLength.ToString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryParse, culture, format

Copy link
Contributor Author

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";

Copy link
Member

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Member

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();
Copy link
Member

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".

Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many bytes written

Copy link
Member

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();
Copy link
Member

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

@cesarblum
Copy link
Contributor Author

@Tratcher I've addressed your comments.

var responseHeaders = FrameResponseHeaders;
if (responseHeaders != null && responseHeaders.HasContentLength)
{
_responseContentLength = long.Parse(responseHeaders.HeaderContentLength.ToString(), NumberStyles.None);
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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

@cesarblum
Copy link
Contributor Author

Updated.

@cesarblum
Copy link
Contributor Author

Added validation to header setter. Not sure I covered all bases though. I'm thinking of adding a long _contentLength field to FrameHeaders, this would give fast access to that value instead of having to parse it from HeaderContentLength.

@@ -201,6 +202,7 @@ public StringValues HeaderContentLength
}
set
{
ValidateLongValue(value);
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

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.

@cesarblum
Copy link
Contributor Author

Updated. Caching Content-Length and wrapping FormatException in InvalidOperationException.

@cesarblum cesarblum force-pushed the cesarbs/track-response-content-length branch from 1aa76e4 to a837285 Compare October 10, 2016 19:45
@@ -12,6 +13,7 @@ public partial class FrameRequestHeaders

private long _bits = 0;
private HeaderReferences _headers;
private long? _contentLength;
Copy link
Member

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")?

Copy link
Member

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.

Copy link
Contributor Author

@cesarblum cesarblum Oct 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done a7e7044

@cesarblum cesarblum force-pushed the cesarbs/track-response-content-length branch from a837285 to a7e7044 Compare October 10, 2016 21:57
@cesarblum
Copy link
Contributor Author

I'm applying the FrameHeaders change to FrameResponseHeaders only (1d3b71d)

"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

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.

Copy link
Contributor Author

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", () => $@"
Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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?

Copy link
Member

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...

Copy link
Contributor Author

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.

Copy link
Member

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().

Copy link
Contributor Author

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.

@halter73
Copy link
Member

I don't think we need a new event id for this. Treating it as a normal application error is fine with me.

@cesarblum
Copy link
Contributor Author

Updated.

@cesarblum cesarblum force-pushed the cesarbs/track-response-content-length branch from 8e1eb31 to 6412753 Compare October 11, 2016 17:22
@halter73
Copy link
Member

:shipit:

{header.SetBit()};
_headers._{header.Identifier} = value; {(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
}}
}}")}
{(loop.ClassName == "FrameResponseHeaders" ? "public long? HeaderContentLengthValue => _contentLength;" : "")}
Copy link
Member

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;
Copy link
Member

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", () => $@"
Copy link
Member

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);")}
Copy link
Member

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", () => $@"
Copy link
Member

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", () => $@"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@cesarblum
Copy link
Contributor Author

@halter73 nits addressed :)

@cesarblum cesarblum force-pushed the cesarbs/track-response-content-length branch from 2e6ffce to f8813a6 Compare October 11, 2016 20:25
@cesarblum cesarblum merged commit f8813a6 into dev Oct 11, 2016
@cesarblum cesarblum deleted the cesarbs/track-response-content-length branch October 11, 2016 20:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants