Skip to content

Add CloseableConnection capability#94

Draft
gjcairo wants to merge 1 commit into
worktree-connection-lifecyclefrom
closeable-connection-followup
Draft

Add CloseableConnection capability#94
gjcairo wants to merge 1 commit into
worktree-connection-lifecyclefrom
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
/// `true` when the request `.end` has been received on the inbound side, or no
/// request is currently in flight. `false` between receiving a request `.head`
/// and its `.end`.
private var requestEndReceived: Bool = true

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.

Why is the default value true?

if case .buffering = self.finalResponseState {
self.flushBuffer(context: context)
}
context.flush()

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.

If self.flushBuffer() was called above, then context.flush() will be called twice (self.flushBuffer() also calls context.flush()).

// The server requires a NIOAsyncChannel, so we create one from the test channel
let serverTestAsyncChannel = try await testChannel.eventLoop.submit {
try NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>(
try NIOAsyncChannel<NIOHTTPServer.HTTP1ChildConnection, Never>(

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.

nit:

Suggested change
try NIOAsyncChannel<NIOHTTPServer.HTTP1ChildConnection, Never>(
try NIOAsyncChannel<HTTP1ChildConnection, Never>(

Comment on lines +203 to +209
/// The context's ``ConnectionContext/signalConnectionClose()`` synchronously
/// flips the shared close flag the channel's ``HTTPKeepAliveHandler``
/// observes when writing the next response head. The synchronous set
/// side-steps any race between firing a NIO event off-loop and writing the
/// response head off-loop. The handler reacts by amending the next response
/// head with `Connection: close` and closing the channel once the response
/// `.end` is 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.

nit: I think this detail is unnecessary for this function.

Comment on lines +387 to +392
/// If the request handler invokes
/// ``ConnectionContext/signalConnectionClose()`` while running, the
/// connection-context's `closeAction` fires `ChannelShouldQuiesceEvent` on
/// the connection-channel pipeline; NIO's HTTP/2 connection management
/// handler reacts by sending `GOAWAY`, letting other in-flight streams
/// complete normally, and finally closing the connection.

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.

nit: I think this detail is unnecessary for this function.

Comment on lines +355 to +358
request,
requestContext,
reader,
responseSender in

@aryan-25 aryan-25 Jul 6, 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.

formatting nit: it should be possible to put all these arguments on the same line. request and reader can be replaced with _ if the line length becomes too long. The same applies for all the other test cases below.

Comment on lines +448 to +452
#expect(response.headerFields[.connection] == "close")
// Look for any duplicate `Connection` headers; the field should
// appear exactly once with value "close".
let connectionValues = response.headerFields[values: .connection]
#expect(connectionValues == ["close"])

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.

Suggested change
#expect(response.headerFields[.connection] == "close")
// Look for any duplicate `Connection` headers; the field should
// appear exactly once with value "close".
let connectionValues = response.headerFields[values: .connection]
#expect(connectionValues == ["close"])
// Look for any duplicate `Connection` headers; the field should
// appear exactly once with value "close".
let connectionValues = response.headerFields[values: .connection]
#expect(connectionValues == ["close"])

Comment on lines +457 to +458
let _trailing = try await iterator.next()
#expect(_trailing == nil)

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.

nit: the leading underscore in the variable name seems unnecessary. The same applies for the test case below.

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.

Comment on lines +500 to +506
// We deliberately don't assert anything about the `Connection`
// header here: `responseSender.send(_:)` returns once the head
// is yielded to the async-stream queue, which may be before the
// keep-alive handler has actually processed it on the event
// loop. So the flag set immediately after `send` is sometimes
// still observed in time to amend the head. The reliable
// observable is that the channel closes after the response.

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 can bypass this by creating a promise that the client will fulfil after it receives the head. The server should await that promise before calling requestContext.signalConnectionClose().

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.

2 participants