-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Customizing HTTP/2 abort response #10886
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
Comments
How does it do that? It should only work if they send the trailers first as trailers after RST would be invalid.
I could image having an HTTP Feature and response extension for providing a Reset method with a custom error code (and another one for GoAway). Also including trailers would be complicated. You still have a multi-threading race condition here though. Only one thread is supposed to touch HttpContext, so calling Reset from a timer thread would not be safe. |
HEADER frame first, then RST_STREAM
Does that include HttpContext.Abort? I don't mind if regular ASP.NET Core thread throws an error because abort has been called. How would you suggest implementing deadline functionality that I've described without another thread? |
Yes, right now it does. Let's discuss the thread safety issue over on #9239 (comment). |
Could we just add a new HTTP/2-specific feature that allows you to both abort the request and configure RST_STREAM all at once? Basically it would be a specially HTTP/2 version of support with extra parameters. Since you already control the timer callback that calls IHttpRequestLifetimeFeature.Abort(), I don't see the need for a callback-based API. |
|
RE: Trailers, sending them would be a protocol violation if the response had a content-length and you hadn't already finished the body. |
Ok, I've refactored gRPC deadline logic so that it no longer uses a Timer plus HttpContext.Abort PR here - https://github.com/grpc/grpc-dotnet/pull/301/files There is now a separate path the server uses when there is a deadline. It produces a better response than HttpContext.Abort, but it allocates a lot more.
Deadline related code here: I do not know whether this a good idea. It would be great to have input from experts in this area. |
I don't think my refactor in gRPC will work. It results in holding onto the HttpContext/Http2Stream after the request pipeline has finished. Calling HttpContext.Abort is not a solution because trailers need to be written and they aren't when the request is aborted. I think the solution is some changes in Kestrel and this issue's original request is the best way forward I can think of:
Good idea? Bad idea? |
Alternatively, an option could be to use |
If BodyWriter.Complete() was used to send the trailers, that would result in an a frame with an end-of-stream flag. I'm not sure it's valid to send an RST stream after that.
This seems necessary.
@Tratcher and I were discussing this yesterday. This seems weird to us. What's the other gRPC implementation that does this? |
It is valid.
https://httpwg.org/specs/rfc7540.html#rfc.section.8.1
The primary gRPC implementation (C-core) does this. In this Wireshark trace of the deadline response from C-core you can see trailers followed by a RST_STREAM |
Here are 3 scenarios around deadline, and what C-core does in each:
Note that gRPC never uses content-length. content-length is not a problem for us but I understand that it something you will need to validate for other users. |
Proposal (high-level, name and shape TBD):
How gRPC would use these in the scenarios mentioned above:
private Timer DeadlineTimer = new System.Threading.Timer(DeadlineExceeded, DeadlineDuration);
public async Task DeadlineExceeded()
{
lock (DeadlineLock) // SemaphoreSlim?
{
// Set DeadlineExpired status
HttpContext.Response.ConsolidateTrailers(this);
// Send remaining content, END_STREAM=true. No further content can be written
// Returns once content and trailers have been sent
await HttpContext.Feature.Get<ICompleteThingFeature>().CompleteAsync();
// Send RST_STREAM (NO_ERROR)
HttpContext.Feature.Get<IResetThingFeature>().ResetHttp2Stream(Http2ErrorCode.NoError);
}
} |
Something else that came up when James and I discussed this is that the GRPC handler will manage the synchronization such that the timeout code path and the normal response code path are never trying to write headers, trailers or body at the same time. These are all discrete operations that happen within the handler and not out in user code. |
Note: CompleteAsync isn't HTTP/2 specific, it could equally apply for HTTP/1.1 chunked responses with or without trailers.
What do the GRPC components do in general if run on a server other than Kestrel? |
That question makes it unfit to go onto HttpResponse. |
Perhaps, it depends on the answer. |
Functional tests run in TestServer. When gRPC uses an API that isn't properly supported in TestServer, e.g. IResponseTrailersFeature, then I improve TestServer. I'll do the same with the new feature that comes out of this issue. |
You could fall back to an HttpResponse.CompleteAsync() that just calls FlushAsync() and then Complete() on the response body PipeWriter. It would still result in unnecessary empty END_STREAM frames, but it's not exactly wrong. But what if instead we added an API similar to the proposed AbortHttp2Stream, but instead of aborting the stream immediately, it buffered the RST frame after the response body and trailers if any? |
@JamesNK I was referring more to IIS or HttpSys. Does gRPC fail or fall back gracefully to other mechanisms? |
Do they support HTTP/2 and trailers today? We haven't tested much outside of Kestrel/TestServer. I don't know. For this feature, what I could do is check to see whether the feature is in HttpContext.Features. If it is then CompleteAsync/AbortHttp2Stream could be used. If it is not then the gRPC would fallback to HttpContext.Abort. |
@JamesNK they do support HTTP/2 but not trailers. |
gRPC would fail on IIS when it calls HttpResponse.AppendTrailer (unless IIS has a dummy IResponseTrailersFeature that no-ops). That's fine. gRPC doesn't work without trailers. |
What is the next step here? I can organize a meeting to come up with a design, or you could guys could informally talk about it and come up with one that I double check. Up to you 🤷♂ |
@JamesNK setup a design meeting so we can flesh this out this week? |
What the |
@halter73 except it's an open set, you could send custom error codes, or quick error codes. It's only HTTP/2 specific if we provide an enum with the spec values. |
I guess the question is then do we expect to use this feature for any other protocol that happens to support integer error codes in some sort of reset frame/message. I don't find this super likely, and I like the somewhat self-documenting nature of including "Http2" in the feature interface name. Just removing it from the HTTP/1 feature collection doesn't really help. It doesn't affect the discoverability of the feature at all unless you happen to be looking at the feature collection in a debugger, so I think having a name indicating this is a feature for HTTP/2 will help avoid confusion. |
CompleteAsync is turning out much easier than I was expecting. I should have a PR by the end of the day. One side effect that we did not discuss: RequestAborted will no longer fire after calling CompleteAsync. This logic was already in place for things like ContentLength responses so you wouldn't get aborted for graceful disconnects after you've sent the full response. CompleteAsync is another way of saying you've sent the whole response. This conflicts with the gRPC scenario that wants to complete the response and then abort. Abort will still send a reset to the client, but RequestAborted does not fire. Any ongoing request body reads fail with OperationCancelledException. Option A) Do nothing in Kestrel. gRPC should add their own chained token to notify the application when gRPC is aborting the response. This would integrate reasonably well with the timeout. |
I also think I prefer option A. After all, from Kestrel's perspective, if you call CompleteAsync() was the request really aborted? I agree this shouldn't be treated differently than writing out the full content-length response. |
With option A there will be a performance penalty in gRPC because we'll have to create our own CTS rather than reusing Kestrel's. I don't think this is a big deal. It is pay-for-play: it will only affect perf when using a deadline. |
You already have the overhead of a timer, you could replace that with the CTS and likely not add any additional overhead. |
Maybe. We need to be able to distinguish between the request being aborted and deadline being exceeded. Might be able to do that with a single CTS - I'll investigate when switching to use CompleteAsync. |
Picking up from #10886 (comment) Looking closer at QUIC, it defines the RST_STREAM frame, but not the individual error code values.
The actual error codes are defined in the HTTP/3 spec. In this case a more generic reset makes sense, the values are disconnected from the functionality. |
If you think it's generic enough, I'm fine with |
I want to add the same features to TestServer quick. |
Closing this out. The TestServer work is going to be more involved than I'd thought. |
Is there an issue for test server implementation? |
Uh oh!
There was an error while loading. Please reload this page.
In gRPC there is the concept of a deadline. The deadline is sent by the client to the server with a gRPC call in a header. It is the client saying to the server "you have X seconds to finish this call, after-which give up".
Today the gRPC server is finishing the connection when the deadline is hit by calling
HttpContext.Abort()
. This immediately sends RST_STREAM with an INTERNAL_ERROR error code. The request delegate with the user's code is still running. UsingAbort
is not ideal, but its response seems to be acceptable to the various gRPC clients of the world.Today that response on Abort looks very hard-coded in Http2Stream:
I think the current abort behavior is fine for gRPC to use with deadline, the one thing that could be improved is the abort response over HTTP/2. We're sending INTERNAL_ERROR, and no status trailers. Another gRPC implementation I looked at immediately ends the response, but it sends RST_STREAM (NO_ERROR), along with some HTTP/2 trailing headers.
It would be nice to have a hook in ASP.NET Core to modify the response when an HTTP/2 abort occurs, such as changing the
Http2ErrorCode
used with RST_STREAM, or appending trailers.Thoughts around the area?
Or alternatives?
The text was updated successfully, but these errors were encountered: