Repository navigation
Reset HTTP/2 stream if an error response ends before the request body - #943
Open
beshkenadze wants to merge 1 commit into
Open
beshkenadze wants to merge 1 commit into
beshkenadze wants to merge 1 commit into
Conversation
Motivation: A server can send a complete response before the client sends the whole request body. An example is a 401 response to an upload without credentials. RFC 9113 section 8.1 permits this. If the response status is >= 300, `HTTPRequestStateMachine` finishes the request and returns `.forwardResponseEnd(.close, ...)`. `HTTP2ClientRequestHandler` ignores `.close`, because it expects that every h2 stream closes after the request. This is not true here. The client stops the request body, but it never sends END_STREAM or RST_STREAM. The stream stays half-closed (remote). `HTTP2IdleHandler` gets no `StreamClosedEvent` for this stream. The connection counts the stream as open, and `HTTPClient.shutdown()` does not complete until the server closes the connection. Modifications: - `HTTP2ClientRequestHandler` sets the new `requestEndSent` flag when it writes the request END_STREAM. The flag tells the handler if the client side of the stream is still open. - On `.close`, if `requestEndSent` is false, the handler closes the stream channel. NIOHTTP2 then sends RST_STREAM(CANCEL). The request body is not complete, so END_STREAM is not correct here. - If `requestEndSent` is true, the handler ignores `.close` as before. The stream closes when both END_STREAM flags pass. A close at this point makes NIOHTTP2 send RST_STREAM on a closed stream. - The handler closes the stream one event loop tick later. It closes the stream only if the handler is still in the pipeline and the stream channel is still active. A server can send the complete response and RST_STREAM(NO_ERROR) in the same read, as RFC 9113 section 8.1 suggests. NIOHTTP2 gives the response end to the handler before it marks the stream as closed. An immediate close then sends RST_STREAM(CANCEL) as a reply to RST_STREAM. RFC 9113 section 5.4.2 does not permit this. One tick later, NIOHTTP2 has closed the stream, and the handler does nothing. - Add two `EmbeddedChannel` tests. The first test makes sure that the handler closes the stream after an early 401 response. The second test makes sure that a response after the request end does not close the stream. Both tests run the event loop before they examine the channel. - Add two tests that connect a client and a server `NIOHTTP2Handler` through `EmbeddedChannel`s and record the RST_STREAM frames of the client. After an early 401 response, the client writes RST_STREAM(CANCEL). If the server sends RST_STREAM(NO_ERROR) in the same read as the 401 response, the client writes no RST_STREAM. Result: After an early response with status >= 300, the client resets the stream. The connection releases the stream, and `HTTPClient.shutdown()` can complete. If the server resets the stream in the same read, the client does not reply with RST_STREAM. Responses after the request end, early 2xx responses, failures and cancellation do not change.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #940
Motivation
After an early HTTP/2 response with status >= 300,
HTTPRequestStateMachinereturns.forwardResponseEnd(.close, ...). Since #657,HTTP2ClientRequestHandlerignores.close. The stream stays half-closed (remote), andHTTPClient.shutdown()waits until the connection closes.Modifications
HTTP2ClientRequestHandlersetsrequestEndSentwhen it writes the request END_STREAM.requestEndSent.closefalsetrue.close, as before.The handler schedules the close with
eventLoop.assumeIsolated().execute, so the close runs after the current read. It runs only if the handler is still in the pipeline and the stream channel is active.Reason: a server can send RST_STREAM(NO_ERROR) after a complete response (RFC 9113, section 8.1). If both come in one read:
4 new tests in
HTTP2ClientRequestHandlerTestseach fail without their part of the change:testStreamIsClosedIfErrorResponseEndsBeforeRequestBodyIsSentmaintestStreamIsResetIfErrorResponseEndsBeforeRequestBodyIsSentNIOHTTP2Handler: the client sends 1 RST_STREAM(CANCEL).maintestStreamIsNotClosedIfResponseEndsAfterRequestEndrequestEndSentchecktestStreamIsNotResetIfServerResetsStreamInSameReadAsErrorResponseEndResult
shutdown()can complete.mainand 1.36.2, 4 runs each (2 eager, 2 lazy body)openH2Streamsin theafterWaitline, 30 s after the responseoutcomein theshutdownlineshutdown()does not complete in 10 s, in 8 of 8 runs.shutdown()completes in 0.001 s or less, in 4 of 4 runs (results/h2-reset-stream-fix.jsonl).To repeat in the repro directory:
The README has the lazy body variant.
This PR does not depend on other PRs. #942 changes other parts of the same 2 files. Git merges the 2 branches in either order. With both, the 109
swift test --filter HTTP2tests pass.