Skip to content

Add CloseableConnection capability - #94

Open
gjcairo wants to merge 9 commits into
mainfrom
closeable-connection-followup
Open

Add CloseableConnection capability#94
gjcairo wants to merge 9 commits into
mainfrom
closeable-connection-followup

Conversation

@gjcairo

@gjcairo gjcairo commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

This is a follow-up of the connection lifecycle PR: #92

It adds support for signalling that the current connection should close after the in-flight response via a new CloseableConnection HTTPServerCapability.
NIOHTTPServer.RequestContext conforms to it and exposes this capability via requestContext.signalConnectionClose().

@gjcairo
gjcairo requested review from FranzBusch and ehaydenr July 6, 2026 09:59
@gjcairo gjcairo added the 🆕 semver/minor Adds new public API. label Jul 6, 2026
Comment thread Sources/NIOHTTPServer/HTTPKeepAliveHandler.swift Outdated
Comment thread Sources/NIOHTTPServer/HTTPKeepAliveHandler.swift Outdated
Comment thread Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift Outdated
Comment thread Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift Outdated
Comment thread Tests/NIOHTTPServerTests/ConnectionLifecycleTests.swift Outdated
Comment thread Tests/NIOHTTPServerTests/ConnectionLifecycleTests.swift Outdated
Comment thread Tests/NIOHTTPServerTests/ConnectionLifecycleTests.swift Outdated
Comment on lines +100 to +101
/// - On HTTP/1.1, the response carries `Connection: close` and the
/// channel is closed once the response has been written.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should document that signalConnectionClose() must be called before sending the response for this to hold true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Unless I'm misunderstanding what you're saying, this isn't true. We can call signalConnectionClose() mid-response and that's okay, we will close once response has been written.

@aryan-25 aryan-25 Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I was talking about initiating connection close after the response header has been flushed.

I think this doc should be updated with these two things:

  • The Connection: close header cannot be written if the response head was already flushed before signalConnectionClose() being called.
  • In this case though, the channel will be closed after the end part is written (following from your change in commit d0688c1).

Comment thread Tests/NIOHTTPServerTests/ConnectionLifecycleTests.swift Outdated
Base automatically changed from worktree-connection-lifecycle to main July 14, 2026 17:37
@gjcairo
gjcairo force-pushed the closeable-connection-followup branch from 08afd1b to 2d04886 Compare July 14, 2026 19:33
@gjcairo
gjcairo marked this pull request as ready for review July 14, 2026 22:13
@gjcairo
gjcairo requested a review from aryan-25 July 14, 2026 22:13
Comment on lines +528 to +533
// Drain the body so `invokeHandler` recovers the iterator and the
// request loop stays alive — otherwise the loop exits when the
// handler returns, and the dispatcher closes the channel anyway.
var body = UniqueArray<UInt8>()
body.reserveCapacity(1024)
_ = try await reader.collect(into: &body)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test seems very similar to the one above. These lines are the only difference I see compared to the test above. But I don't think this should matter because the client in this test case only sends a head and end part.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It does matter because of the way we handle the ReaderState internally when we finish reading vs when we don't (see NIOHTTPServer/invokeHandler). Also, another difference is we're not setting any timeouts in this test: we want to make sure the connection is being closed and not hang indefinitely/be closed by other timeout mechanisms instead of close being signalled.

@gjcairo
gjcairo requested a review from aryan-25 July 15, 2026 12:42

@aryan-25 aryan-25 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you! Just one small comment.

Comment thread Tests/NIOHTTPServerTests/ConnectionLifecycleTests.swift Outdated
Comment on lines +45 to +54
/// A request-context capability that lets a request handler signal that the
/// connection should close after the current response.
///
/// Servers whose request context conforms to this capability allow handlers
/// to indicate that the underlying connection should be closed once the
/// in-flight response has been sent. Implementations make the signal
/// effective on a best-effort basis (HTTP/1.1 typically appends
/// `Connection: close` and closes the channel; HTTP/2 typically sends
/// `GOAWAY` and lets in-flight streams complete normally).
public protocol CloseableConnection: RequestContext {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I find the semantics a bit weird that this only closes after the response is send. Why doesn't it close right away?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For H1, closing after the response is sent allows users to write back a response that will contain the Connection:close header, because we'll set it when signalling close.

For H2, I think it's weird to just abruptly close the connection because there can be other streams open. We currently send GOAWAY but I suppose we could send RST_STREAMs instead?

I think just abruptly closing the connection feels too harsh, but perhaps that's the desired behaviour.

cc @ehaydenr

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you want to be able to do both. If you're shutting down, you may first want to send a graceful GOAWAY to avoid disrupting in flights requests. After some grace period, you may follow up with a more abrupt GOAWAY followed by connection close which would break streams. Alternatively, you may want to immediately close things abruptly if the peer is behaving badly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You think it would make sense to be able to initiate both from here, or would you have separate capabilities?

I've not given this much thought but one thing that kinda bothers me is that force closing doesn't stop you from still attempting to read or write within this handler, and that will cause errors. Basically the only thing you can do after a force close is to return. The only way I can think of doing this is by having the force close consume the reader and response sender/writer, but I don't think that's particularly nice either.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think if you force close and then do a read or write, it should probably result in an error and that's OK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ehaydenr do you wanna be able to do both the graceful shutdown and the force closure from a stream? I am actually wondering if we are doing this the wrong way around. In my opinion, having this as a stream capability is really weird and it would be better served as API on Connection. So what I am suggesting is that Connection gains methods for triggerGracefulShutdown, close and maybe a combined one that triggers graceful shutdown and after a duration closes. You can then inject an MPSC channel into your stream handlers that will do this on the connection. What do you think about this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I want to be able to do it from a request hander. Whether it's exposed as a RequestContext capability or I need to plumb it through to where I instantiate my request handler, I don't think it matters. Exposing as a capability seems like the natural way to accomplish this. What about it do you find weird?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I continue to think it is weird to expose connection lifecycle management methods on request context. I feel like we will end up with a god request that will allow you to do everything with the connection. I much prefer clear separation of concerns between the request handler and the mostly read-only context of the connect and a separate connection lifecycle API. If users want to tie the two together they can setup a communication channel between those.

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

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants