Skip to content

Conversation

@adam-fowler
Copy link
Contributor

Check for half closure during server upgrade and close channel if client closes the channel

Motivation:

This is to fix #2742

Modifications:

Add userInboundEventTriggered function to NIOTypedHTTPServerProtocolUpgrader which checks for ChannelEvent.inputClosed

Result:

Negotiation future now errors when client closes the connection instead of never completing

switch event {
case let evt as ChannelEvent where evt == ChannelEvent.inputClosed:
// The remote peer half-closed the channel during the upgrade so we should close
context.close(promise: nil)
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for working on this!

Can we please go through the state machine here. In particular, if we are in the state unbuffering we should not close here but rather just hold the event until the unbuffering is done.

Can we also write a test for this and look if we need to do the same for the client upgrade handler?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should also consider only closing if we haven't received a full upgrade request, and otherwise we should buffer this.

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 did consider this case, but thought I would post this first to start the conversation. So should we only close the connection if we are in the state initial and awaitingUpgrader. Otherwise should I let it run until the upgrade is complete?

Second all the upgrade tests use EmbeddedChannel. Is it possible to emulate half closure with EmbeddedChannel?

Copy link
Member

Choose a reason for hiding this comment

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

It should be possible to emulate half closure with the embedded channel by just sending the event to the channel.

@Lukasa Lukasa added the 🔨 semver/patch No public API change. label Jun 27, 2024
@inlinable
mutating func closeInbound() -> CloseInboundAction {
switch self.state {
case .initial, .awaitingUpgrader:
Copy link
Member

Choose a reason for hiding this comment

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

We should immediately close in the state initial. In the state awaitingUpgrader we have to check if we have seenFirstRequest == true. If have seen that we should buffer the inputClosed and send it after the upgrading is finished. In the state upgraderReady we need to buffer the inputClosed indefinitely.

The reason why this is so complicated is that the client could potentially send a half closure after it send the initial upgrade request + some data on the upgraded protocol. We can't just unconditionally close here but we rather need to buffer the inputClose and unbuffer it with the data. I would recommend changing the var buffer: Deque<NIOAny> in the various states to var buffer: Deque<enum(NIOAny | inputClosed)>.

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!

try connectedServer.pipeline.waitForUpgraderToBeRemoved()
}

func testHalfClosure() throws {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add one or two more tests cases here:

  1. That sends the request head then input close
  2. That sends a full request head & end and then input close to check that we continue the upgrade

Copy link
Contributor Author

@adam-fowler adam-fowler Jul 23, 2024

Choose a reason for hiding this comment

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

@FranzBusch
I did 2 (see testSendRequestCloseImmediately)

I'm not sure what the expected result is for 1
First I had to send a request head that included a content-length so a .head would be passed down the pipeline, but not an.end eg

OPTIONS * HTTP/1.1\r\nHost: localhost\r\ncontent-length: 10\r\nUpgrade: myproto\r\nKafkaesque: yup\r\nConnection: upgrade\r\nConnection: kafkaesque\r\n\r\n

With this it gets stuck in the state .upgradeReady because nothing else happens after closeInbound to forward the state machine onwards and the connection stays open. If I return .close when the state is .upgradeReady from the state machine everything works but you explicitly said I shouldn't do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@FranzBusch any thoughts

@adam-fowler adam-fowler force-pushed the websocket-half-close branch from 67750ef to 96fb6ea Compare July 23, 2024 12:43
@adam-fowler adam-fowler requested a review from FranzBusch July 23, 2024 12:49
@weissi
Copy link
Member

weissi commented Aug 30, 2024

ping @FranzBusch and @Lukasa

Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

Sorry for the delay. We are really close here. Just a few last comments

Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

This looks great. One last small comment!

Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

Great! Thanks for working on this.

@FranzBusch FranzBusch enabled auto-merge (squash) November 13, 2024 15:27
@FranzBusch FranzBusch disabled auto-merge November 13, 2024 15:54
@FranzBusch FranzBusch enabled auto-merge (squash) November 13, 2024 15:54
auto-merge was automatically disabled November 13, 2024 16:06

Head branch was pushed to by a user without write access

@FranzBusch FranzBusch enabled auto-merge (squash) November 14, 2024 17:59
@FranzBusch FranzBusch merged commit a026de3 into apple:main Nov 14, 2024
41 of 43 checks passed
Lukasa pushed a commit to Lukasa/swift-nio that referenced this pull request Nov 22, 2024
Check for half closure during server upgrade and close channel if client
closes the channel

### Motivation:

This is to fix apple#2742

### Modifications:

Add `userInboundEventTriggered` function to
`NIOTypedHTTPServerProtocolUpgrader` which checks for
`ChannelEvent.inputClosed`

### Result:

Negotiation future now errors when client closes the connection instead
of never completing

---------

Co-authored-by: Franz Busch <[email protected]>
(cherry picked from commit a026de3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NIOTypedWebSocketServerUpgrader hangs when connection is opened and closed without sending any data

4 participants