Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send RST_STREAM instead EOF when sending request body is cancelled #58548

Merged

Conversation

alnikola
Copy link
Contributor

@alnikola alnikola commented Sep 2, 2021

Fixes #58498

@alnikola alnikola added this to the 7.0.0 milestone Sep 2, 2021
@alnikola alnikola requested a review from a team September 2, 2021 11:16
@ghost
Copy link

ghost commented Sep 2, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #58498

Author: alnikola
Assignees: -
Labels:

area-System.Net.Http

Milestone: 7.0.0

@alnikola
Copy link
Contributor Author

alnikola commented Sep 2, 2021

I need to say that I still have a concern regarding how Kestrel will react to such RST_STREAM with CANCEL. We expect that it will continue sending the response body (if any), but we need to verify it.

@alnikola alnikola added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Sep 2, 2021
@alnikola alnikola removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Sep 6, 2021
@alnikola
Copy link
Contributor Author

alnikola commented Sep 6, 2021

@geoffkizer I discussed this behavior with the Kestrel team and we decided that when the request body is skipped the RST_STREAM with CANCEL should be sent after the response body receival has completed. This is required by the HTTP/2 stream states transition rules as described RFC 7540 section 5.1.

cc: @Tratcher

If this state is reached as a result of sending a RST_STREAM
frame, the peer that receives the RST_STREAM might have already
sent -- or enqueued for sending -- frames on the stream that
cannot be withdrawn. An endpoint MUST ignore frames that it
receives on closed streams after it has sent a RST_STREAM frame.
An endpoint MAY choose to limit the period over which it ignores
frames and treat frames that arrive after this time as being in
error.

@alnikola
Copy link
Contributor Author

alnikola commented Sep 6, 2021

/azp run runtime-libraries stress-http

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

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

Looks good, just one question. Thanks!

if (frame == null && expectEndOfStream)

// Check if it was a zero-byte read or we got a RST_STREAM with the CANCEL code.
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
Copy link
Member

@ManickaP ManickaP Sep 7, 2021

Choose a reason for hiding this comment

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

Just a suggestion. I had to lookup operator precedence of && and || to make sure I understand the condition correctly.

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if ((frame == null || (frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8)) && expectEndOfStream)

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 see the confusion, but then we need to add even more parenthesis because there are more || and && operations. Don't you think it could make the code less readable?

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if ((frame == null || ((frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8)) && expectEndOfStream))

Copy link
Member

Choose a reason for hiding this comment

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

Now I'm fully confused what the right order of evaluation is...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

&& takes precedence over || so the order is

  1. frame == null
    OR
  2. frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8 && expectEndOfStream

Copy link
Member

@karelz karelz Sep 7, 2021

Choose a reason for hiding this comment

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

What about this?

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if ((frame == null
|| (frame.Type == FrameType.RstStream
&& ((RstStreamFrame)frame).ErrorCode == 0x8))
&& expectEndOfStream)

Or for simplicity, move expectedEndOfStream to the beginning:

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if (expectEndOfStream
&& (frame == null
|| (frame.Type == FrameType.RstStream
&& ((RstStreamFrame)frame).ErrorCode == 0x8)))

Copy link
Member

Choose a reason for hiding this comment

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

I think we need to change the code to make order of || and && explicit. It is not something anyone wants to keep looking up and I bet that 99% of developers don't remember it. I will rather survive more parenthesis than mind-blowing thinking about || and && precedence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This way then?

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if (expectEndOfStream
&& (frame == null || (frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8)))

Copy link
Member

@karelz karelz Sep 7, 2021

Choose a reason for hiding this comment

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

IMO at minimum this:

Suggested change
if ((frame == null || frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8) && expectEndOfStream)
if (expectEndOfStream
&& (frame == null || (frame.Type == FrameType.RstStream && ((RstStreamFrame)frame).ErrorCode == 0x8)))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

Choose a reason for hiding this comment

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

Haha, that's what I suggested initially (with the exception of moving expectEndOfStream at the beginning) 😄

@@ -1307,6 +1316,13 @@ private void CloseResponseBody()
{
Cancel();
}
else if (_sendRstOnResponseClose)
Copy link
Member

Choose a reason for hiding this comment

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

This is called with response Dispose. Which works great for cancelling partially consumed responses. But for fully consumed response - if the user doesn't Dispose the response - we would be waiting for GC to collect the stream and send the RST. Shouldn't we rather send it immediately after we reach the end of response stream?

Note that I'm not saying this is wrong, just thinking of possible scenarios that could happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. You are right, delaying sending seems as an issue. Do you know how and where we can react to reaching the end of response stream?

Copy link
Member

Choose a reason for hiding this comment

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

I'd say in this else block:

else
{
// We've hit EOF. Pull in from the Http2Stream any trailers that were temporarily stored there.
MoveTrailersToResponseMessage(responseMessage);
}

@geoffkizer
Copy link
Contributor

@geoffkizer I discussed this behavior with the Kestrel team and we decided that when the request body is skipped the RST_STREAM with CANCEL should be sent after the response body receival has completed. This is required by the HTTP/2 stream states transition rules as described RFC 7540 section 5.1.

Sounds right to me.

@alnikola alnikola merged commit c462a68 into dotnet:main Sep 8, 2021
@alnikola alnikola deleted the alnikola/58498-rst-insted-eof-body-not-sent branch September 8, 2021 12:36
@ghost ghost locked as resolved and limited conversation to collaborators Oct 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[HTTP/2] Send RST_STREAM instead EOF if request has Content-Length, but body is not sent
4 participants