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

QUIC: Read data and get end notification together #55707

Closed
JamesNK opened this issue Jul 15, 2021 · 28 comments · Fixed by #57492
Closed

QUIC: Read data and get end notification together #55707

JamesNK opened this issue Jul 15, 2021 · 28 comments · Fixed by #57492
Assignees
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Net.Quic
Milestone

Comments

@JamesNK
Copy link
Member

JamesNK commented Jul 15, 2021

There doesn't seem to be an API on QuicStream that allows you to read data and get a notification that it is the final chunk of data in one operation.

An API for writing data and ending a stream together exists:

await stream.WriteAsync(data, endStream: true);

But for reading you must always get data and then read again to get a result of 0 which means no more data:

var buffer = GetBuffer();
while (true)
{
    var readCount = await stream.ReadAsync(buffer);
    if (readCount == 0) break;

    // Do stuff with data but don't know whether it is the final data.
}

I think this might be required for Kestrel to properly support HEADERS only requests.

When Kestrel gets the first HEADER frame it needs to start the request. But it also needs to know whether it or not is it ONLY a HEADERS request. If the stream data ends with the HEADERS frame then we process it differently than if the stream is still going and there are DATA frames still to come.

Kestrel can't hang around, waiting on the next QuicStream.ReadAsync to return 0 or a value before it starts the request. That data might be a long time coming (e.g. a long-running gRPC stream where the client doesn't send a message on the stream until 1 minute later)

I think we need ReadAsync to return something like ReadResult. ReadResult has data on it (it could be a data length if you prefer), AND a flag saying whether we are done or not.

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.IO untriaged New issue has not been triaged by the area owner labels Jul 15, 2021
@ghost
Copy link

ghost commented Jul 15, 2021

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

Issue Details

I don't know whether this is important, but there doesn't seem to be an API on QuicStream that allows you to read data and get a notification that it is the final chunk of data in one operation.

An API for writing data and ending a stream together exists:

await stream.WriteAsync(data, endStream: true);

But for reading you must always get data and then read again to get a result of 0 which means no more data:

var buffer = GetBuffer();
while (true)
{
    var readCount = await stream.ReadAsync(buffer);
    if (readCount == 0) break;

    // Do stuff with data but don't know whether it is the final data.
}
Author: JamesNK
Assignees: -
Labels:

area-System.IO, untriaged

Milestone: -

@ghost
Copy link

ghost commented Jul 15, 2021

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

Issue Details

There doesn't seem to be an API on QuicStream that allows you to read data and get a notification that it is the final chunk of data in one operation.

An API for writing data and ending a stream together exists:

await stream.WriteAsync(data, endStream: true);

But for reading you must always get data and then read again to get a result of 0 which means no more data:

var buffer = GetBuffer();
while (true)
{
    var readCount = await stream.ReadAsync(buffer);
    if (readCount == 0) break;

    // Do stuff with data but don't know whether it is the final data.
}

I think this might be required for Kestrel to properly support HEADERS only requests.

When Kestrel gets the first HEADER frame it needs to start the request. But it also needs to know whether it or not is it ONLY a HEADERS request. If the stream data ends with the HEADERS frame then we process it differently than if the stream is still going and there are DATA frames still to come.

Kestrel can't hang around, waiting on the next QuicStream.ReadAsync to return 0 or a value because that data might be a long time coming (e.g. a long running gRPC stream where the client doesn't seen a message on the stream until 1 minute later)

Author: JamesNK
Assignees: -
Labels:

area-System.Net.Quic, untriaged

Milestone: -

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

@Tratcher

@geoffkizer
Copy link
Contributor

We could potentially expose an API like this, but I don't think it solves the problem here.

You will still need to handle the case where the headers arrive without a FIN, and then the FIN arrives later.

@Tratcher
Copy link
Member

Related discussion: #55347 (comment).

MSQuic looks like it surfaces this state on StreamEventDataReceive.Flags = FIN, QuicStream does not.

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

My knowledge of QUIC isn't strong, but I believe that in QUIC the data and the end stream flag will always arrive together, like how a HEADERS frame in HTTP/2 would have an END_STREAM flag set or not.

STREAM frame? - https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8

What we need with QuicStream is to know whether the data that has come in has had the FIN set. Right now we don't have that capability.

Now, if someone...

  1. Decides to send HEADERS in a QUIC frame
  2. They intended it to be a HEADERS only request
  3. But that QUIC frame with the HEADERS data doesn't report FIN

...then that is a problem with the peer. Kestrel will report an error when it validates the incoming request.

@geoffkizer
Copy link
Contributor

in QUIC the data and the end stream flag will always arrive together

That's not the case. In QUIC, it is perfectly legal to send some data, wait, then send FIN.

like how a HEADERS frame in HTTP/2 would have an END_STREAM flag set or not.

QUIC packets/frames aren't really analogous to HTTP2 frames. They are much more similar to TCP packets.

HTTP3 frames are analogous to HTTP2 frames -- HEADERS, DATA, etc.

Now, if someone...
Decides to send HEADERS in a QUIC frame
They intended it to be a HEADERS only request
But that QUIC frame with the HEADERS data doesn't report FIN
...then that is a problem with the peer. Kestrel will report an error when it validates the incoming request.

Unless there is some particular provision of HTTP3 that outlaws this that I'm not aware of, then this is a perfectly fine thing to do and Kestrel needs to handle it.

Note this is somewhat analogous to sending an HTTP2 HEADERS frame without EndStream, then immediately sending a 0-length DATA frame with the FIN bit set.

@geoffkizer
Copy link
Contributor

They intended it to be a HEADERS only request

BTW, I don't think there is a notion of a "HEADERS only request" in HTTP3, or really in HTTP itself. There are only requests with 0-length bodies.

There are some ways you can detect a 0-length body just from inspecting the headers -- for example, if the Content-Length header is set to 0. But that's not always the case, regardless of version.

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

Unless there is some particular provision of HTTP3 that outlaws this that I'm not aware of, then this is a perfectly fine thing to do and Kestrel needs to handle it.

It is part of the HTTP/3 spec but it is not spelled out obviously. We need it to properly detect and reject certain malformed requests. An example is someone sending a non-zero content-length in a message with no body. We need to reject with an abort, and we need to do it before executing user app code.

https://datatracker.ietf.org/doc/html/draft-ietf-quic-http-34#section-4.1.3

@geoffkizer
Copy link
Contributor

It is part of the HTTP/3 spec but it is not spelled out obviously.

I'm not seeing it here, can you point to what you mean specifically?

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

A request or response that is defined as having content when it
contains a Content-Length header field (Section 6.4.1 of
[SEMANTICS]), is malformed if the value of a Content-Length header
field does not equal the sum of the DATA frame lengths received.

Scenario: we have HEADERS, they have a content-length of 10, we know we have no DATA frames (the stream has ended), therefore the request is malformed at the point in time the HEADERS frame is received. App code is never executed with the malformed request.

Intermediaries that process HTTP requests or responses (i.e., any
intermediary not acting as a tunnel) MUST NOT forward a malformed
request or response. Malformed requests or responses that are
detected MUST be treated as a stream error (Section 8) of type
H3_MESSAGE_ERROR.

Kestrel is acting as a proxy and should not forward on the malformed request. We stop that from happening by rejecting the request when the HEADERS are received and never executing app code (i.e. the proxy code that will forward the request)

@geoffkizer
Copy link
Contributor

Sure, that makes sense. But how does that apply to the issue at hand here? How is it different than, say, the request body having 1 byte instead of 0?

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

Kestrel will start executing the app code and I believe will error at a different place (on reading the end of the request body? Chris will know). There is no way for a server to pre-emptively detect the request is malformed in that situation.

In the case of the no message body a server can detect it is malformed immediately and reject it before executing app code.

@geoffkizer
Copy link
Contributor

Ok, but again, I think you have to handle the case where FIN is sent in a separate packet. I've yet to see anything that says otherwise. Am I missing something?

In the case of the no message body a server can detect it is malformed immediately and reject it before executing app code.

That sounds nice, but... there are lots of ways a request can be malformed. Why is this case special?

@geoffkizer
Copy link
Contributor

I also want to point out that, while there are scenarios where we know there is no request body a priori, there are also scenarios where we simply don't know. For example, we ask the user's content to serialize, and it results in 0 bytes.

In those cases it's possible and in fact likely that you will receive the FIN separate from the request headers, and it is not correct to reject this as malformed.

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

Ok, but again, I think you have to handle the case where FIN is sent in a separate packet. I've yet to see anything that says otherwise. Am I missing something?

In that case Kestrel will assume there is a body to go with the request header's content-length and will execute the user app code.

@geoffkizer
Copy link
Contributor

Okay, but what is your alternative here? If this is valid client behavior (and I believe it is), then you need to handle it somehow.

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

I don't understand the question. Do you mean this?

I also want to point out that, while there are scenarios where we know there is no request body a priori, there are also scenarios where we simply don't know. For example, we ask the user's content to serialize, and it results in 0 bytes.

I'm guessing the client didn't send a content-length so it doesn't matter whether there is a body present or not.

We have many tests for different content-length scenarios to test we're handling them correctly - https://github.com/dotnet/aspnetcore/blob/main/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs

@geoffkizer
Copy link
Contributor

You claimed above:

Now, if someone...
Decides to send HEADERS in a QUIC frame
They intended it to be a HEADERS only request
But that QUIC frame with the HEADERS data doesn't report FIN
...then that is a problem with the peer. Kestrel will report an error when it validates the incoming request.

Based on my understanding, this is valid client behavior and you need to handle it.

@JamesNK
Copy link
Member Author

JamesNK commented Jul 15, 2021

It will be just like an empty DATA frame with END_STREAM in HTTP/2. The request will make it to user app code and the error will happen later.

@geoffkizer
Copy link
Contributor

Okay, then it seems like we have this handled.

@ManickaP ManickaP removed the untriaged New issue has not been triaged by the area owner label Jul 15, 2021
@ManickaP ManickaP added this to the 6.0.0 milestone Jul 15, 2021
@ManickaP
Copy link
Member

Triage: seems like an API and feature work. Unless we have a failing scenario from a user/customer (H/3 client server) we should leave it to 7.0.

@ManickaP ManickaP modified the milestones: 6.0.0, 7.0.0 Jul 15, 2021
@ManickaP ManickaP added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jul 15, 2021
@JamesNK
Copy link
Member Author

JamesNK commented Aug 7, 2021

I believe I've narrowed down some flakey tests to missing this API.

There are Kestrel tests that are sent GET requests by the client (and so have no request body). The app code does some minimal work on the server. The tests pass is the request was completed gracefully. That means the request was read to the end and the response completes sending

Unfortunately, the app code is completing BEFORE the end of the request. That's because we're getting the content to read the HEADERS frame in one QuicStream.ReadAsync call, and then getting end of stream notification in another QuicStream.ReadAsync call.

There is a race between the app code completing and then second QuicStream.ReadAsync completing. If the app code wins then you see:

[xUnit.net 00:00:04.44] | [0.056s] Microsoft.AspNetCore.Server.Kestrel Information: Connection id "0HMAP7F0FJ55V", Request id "0HMAP7F0FJ55V:00000000": the application completed without reading the entire request body.
[xUnit.net 00:00:04.44] | [0.056s] Microsoft.AspNetCore.Server.Kestrel.Transport.Quic Debug: Stream id "0HMAP7F0FJ55V:00000000" read side aborted by application because: "The application completed without reading the entire request body.".

That happens despite there being no request body to read.

[xUnit.net 00:00:04.44]         | [0.055s] Microsoft.AspNetCore.Server.Kestrel.Transport.Quic Debug: Stream id "0HMAP7F0FJ55V:00000000" type Bidirectional accepted.
[xUnit.net 00:00:04.44]         | [0.055s] Microsoft.AspNetCore.Server.Kestrel.Http3 Trace: Connection id "0HMAP7F0FJ55V" received HEADERS frame for stream ID 0 with length 48.
[xUnit.net 00:00:04.44]         | [0.055s] Microsoft.AspNetCore.Hosting.Diagnostics Information: Request starting HTTP/3 GET https://127.0.0.1:34548/ - -
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Server.Kestrel.Http3 Trace: Connection id "0HMAP7F0FJ55V" sending HEADERS frame for stream ID 0 with length 71.
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Hosting.Diagnostics Information: Request finished HTTP/3 GET https://127.0.0.1:34548/ - - - 200 0 - 0.7542ms
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Server.Kestrel Debug: Connection id "0HMAP7F0FJ55V", Request id "0HMAP7F0FJ55V:00000000": started reading request body.
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Server.Kestrel Debug: Connection id "0HMAP7F0FJ55V", Request id "0HMAP7F0FJ55V:00000000": done reading request body.
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Server.Kestrel Information: Connection id "0HMAP7F0FJ55V", Request id "0HMAP7F0FJ55V:00000000": the application completed without reading the entire request body.
[xUnit.net 00:00:04.44]         | [0.056s] Microsoft.AspNetCore.Server.Kestrel.Transport.Quic Debug: Stream id "0HMAP7F0FJ55V:00000000" read side aborted by application because: "The application completed without reading the entire request body.".
[xUnit.net 00:00:04.44]         | [0.057s] Microsoft.AspNetCore.Server.Kestrel Warning: Stream threw an unexpected exception.
[xUnit.net 00:00:04.44]         | Microsoft.AspNetCore.Connections.ConnectionAbortedException: The application completed without reading the entire request body.
[xUnit.net 00:00:04.44]         |    at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
[xUnit.net 00:00:04.44]         |    at System.IO.Pipelines.Pipe.GetReadAsyncResult()
[xUnit.net 00:00:04.44]         |    at System.IO.Pipelines.Pipe.DefaultPipeReader.GetResult(Int16 token)
[xUnit.net 00:00:04.44]         |    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3Stream.ProcessRequestAsync[TContext](IHttpApplication`1 application) in /_/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs:line 422
[xUnit.net 00:00:04.44]         | [0.057s] Microsoft.AspNetCore.Server.Kestrel.Transport.Quic Debug: Stream id "0HMAP7F0FJ55V:00000000" shutting down writes because: "The QUIC transport's send loop completed gracefully.".

@geoffkizer
Copy link
Contributor

Regardless of whether we add this API, it's still valid HTTP3 to send the FIN separately and you will need to handle this case somehow.

@JamesNK
Copy link
Member Author

JamesNK commented Aug 8, 2021

That's fine. Kestrel won't break or error, but we will be where we are today for all GET requests: there will be a race. Apps will write logs that GET requests weren't completed cleanly which will be confusing. We can't fix it for sub-optimal clients, but we can stop this from happening from those that send HEADERS and FIN together. At the very least, that will include HttpClient - #55347 - and will fix the flakey HttpClient+Kestrel tests that we have.

@geoffkizer
Copy link
Contributor

We can't fix it for sub-optimal clients,

Why not?

@JamesNK
Copy link
Member Author

JamesNK commented Aug 8, 2021

Because Kestrel starts processing the app delegate on a different thread as soon as HEADERS are received. The server can't hang around, waiting for the next QuicStream.ReadAsync result. Processing immediately after HEADERS are received is part of the HTTP/3 spec.

After that point it is a race between IO thread figuring out there is no body when the next QuicStream.ReadAsync call returns 0, and app delegate thread finishing and checking to see whether the app code read to the end of the request stream.

If you want to keep discussing this then I suggest a meeting. Explaining Kestrel's architecture and challenges one comment at a time will take forever.

There is also a WIP branch that adds a QuicStream property that indicates whether you have read to the end of the stream. That will let us fix this. We could use some help with it.

@geoffkizer
Copy link
Contributor

The server can't hang around, waiting for the next QuicStream.ReadAsync result.

Why not? The server is just generating an event here, right? Why is it hard to defer generating this event until you determine whether there is actually a request body or not?

Processing immediately after HEADERS are received is part of the HTTP/3 spec.

I'm not sure what this means; can you explain?

If you want to keep discussing this then I suggest a meeting.

Great, let's set up a meeting.

I will say, it seems strange to me that we would generate an event like this in a case where (a) the client is behaving correctly, as per RFC, and (b) the user's code on the server is also behaving reasonably. As a user, how is this event helpful to me? What sort of action should I take?

@CarnaViire CarnaViire self-assigned this Aug 13, 2021
@CarnaViire CarnaViire modified the milestones: 7.0.0, 6.0.0 Aug 13, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Aug 16, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Aug 17, 2021
CarnaViire added a commit that referenced this issue Aug 17, 2021
Add ReadsCompleted API that exposes ReadState=ReadsCompleted, set ReadState to ReadsCompleted if FIN flag arrives in RECEIVE event, fix ReadState changing to final stauses, expand ReadState transition description

Fixes #55707
@ghost ghost locked as resolved and limited conversation to collaborators Sep 16, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Net.Quic
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants