Skip to content

Keep the HTTP/2 request until its request body stream ends - #942

Open
beshkenadze wants to merge 1 commit into
swift-server:mainfrom
beshkenadze:fix/h2-keep-request-until-upload-ends
Open

beshkenadze wants to merge 1 commit into
swift-server:mainfrom
beshkenadze:fix/h2-keep-request-until-upload-ends

Conversation

@beshkenadze

Copy link
Copy Markdown

Fixes #922

Motivation

On HTTP/2, a server can end a response with a status below 300 before the request body ends. Since #879 (1.31.0), HTTPRequestStateMachine then returns .forwardResponseEnd(.none, ...). The request body continues, but HTTP2ClientRequestHandler sets self.request to nil. The next action force unwraps nil:

  • .failRequest crashes at HTTP2ClientRequestHandler.swift:249. A stream reset, a connection close or a GOAWAY with a lower last stream ID causes it. This is the crash in Crash in HTTP2ClientRequestHandler #922.
  • .resumeRequestBodyStream crashes at line 228 when the channel becomes writable again.

Before the crash, the handler rejects the remaining body parts with HTTPClientError.requestStreamCancelled and ignores the request end. The client sends no END_STREAM and no RST_STREAM. The stream stays half-closed (remote).

Changes

  • After .forwardResponseEnd with the final action .none, the handler keeps the request. HTTP1ClientChannelHandler does the same.
  • After .sendRequestEnd with the final action .requestDone or .close, the handler releases the request.

5 new tests in HTTP2ClientRequestHandlerTests end a 200 response while the body streams. Then each test does one step:

  • Makes the channel writable again.
  • Sends a stream reset error (NIOHTTP2Errors.StreamClosed with CANCEL).
  • Sends a channelInactive event.
  • Sends the remaining body and the request end.
  • Writes a body part after the request end.

Result

On macOS 27.0.1 with Swift 6.4:

Handler HTTP2ClientRequestHandlerTests
0171152 3 new tests crash (line 228 one time, line 249 two times). 2 new tests fail.
#933 1 new test crashes at line 228. 4 new tests fail. The request does not get the stream error. The client does not send the remaining body and END_STREAM.
This PR The 15 tests pass.
git checkout 0171152 -- Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
swift test --parallel --filter HTTP2ClientRequestHandlerTests   # 3 crashes, 2 failures
git fetch https://github.com/swift-server/async-http-client pull/933/head
git checkout FETCH_HEAD -- Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
swift test --parallel --filter HTTP2ClientRequestHandlerTests   # handler of #933: 1 crash, 4 failures
git checkout HEAD -- Sources/AsyncHTTPClient/ConnectionPool/HTTP2/HTTP2ClientRequestHandler.swift
swift test --parallel --filter HTTP2ClientRequestHandlerTests   # 15 tests pass

Other PRs

Motivation:

An HTTP/2 server can send a complete response with a status below 300
while the client still streams the request body. In this case,
`HTTPRequestStateMachine` returns `.forwardResponseEnd(.none, ...)`. The
`.none` action means that the request is not done yet. The request body
stream continues.

`HTTP2ClientRequestHandler` ignored this action and released the
request. The state machine still expected the request. This caused
three failures:

- The channel became writable again and the state machine resumed the
  request body stream. The handler force unwrapped the released request
  and crashed.
- A stream reset or a connection close failed the request. The handler
  force unwrapped the released request and crashed. This is the crash in
  swift-server#922.
- The handler dropped the remaining request body parts without sending
  them. It also dropped the request body end. It did not write
  END_STREAM, so the HTTP/2 stream stayed half-open. `Transaction` and
  `RequestBag` did not report an error to the body writer.

Modifications:

- Keep the request when the final action of `.forwardResponseEnd` is
  `.none`. `HTTP1ClientChannelHandler` does the same.
- Release the request in `.sendRequestEnd` when the final action is
  `.requestDone` or `.close`.
- Add tests for a resumed request body stream, a stream reset, a
  connection close and the request body end after an early response end.
- Add a test for a request body part that arrives after the request end.
  The handler must reject the part with
  `HTTPClientError.requestStreamCancelled`. Without the release in
  `.sendRequestEnd`, the idle write timeout state machine crashes.

Result:

After an early response end with a status below 300, the HTTP/2 handler
sends the remaining request body and END_STREAM. Then the stream closes.
A stream reset or a connection close fails the request and does not
crash the client. The behavior for responses with a status of 300 or
more does not change.

Fixes swift-server#922.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash in HTTP2ClientRequestHandler

1 participant