Skip to content

Add connection lifecycle handling - #92

Merged
rafaelcepeda merged 9 commits into
mainfrom
worktree-connection-lifecycle
Jul 14, 2026
Merged

Add connection lifecycle handling#92
rafaelcepeda merged 9 commits into
mainfrom
worktree-connection-lifecycle

Conversation

@gjcairo

@gjcairo gjcairo commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add a connection-lifecycle surface to NIOHTTPServer so users can do per-connection setup (such as loggers, metrics, atomic counters, etc) and share state across requests on the same connection. The existing serve(handler:) entry point is preserved unchanged at the call site.

What's new

  • NIOHTTPServer.serve(connectionHandler:): the connection-aware counterpart to serve(handler:).
  • NIOHTTPServerConnectionHandler protocol with handleConnection(connection:context:).
  • NIOHTTPServer.Connection: struct that represents an active HTTP server connection. Consumed by exactly one call to handleRequests; if the handler returns without calling handleRequests, the connection is dropped cleanly on scope exit.
  • NIOHTTPServer.ConnectionContext, which now exposes httpVersion, remoteAddress, localAddress, the existing peerCertificateChain, and a new signalConnectionClose().
  • New HTTPServerCapability capabilities: ConnectionInfo, PeerCertificate. NIOHTTPServer.RequestContext is now a concrete struct conforming them, so request handlers reach connection-scoped data via requestContext.remoteAddress, etc.
  • The @TaskLocal NIOHTTPServer.connectionContext API is removed, and all access is now through requestContext.

gjcairo added 2 commits June 30, 2026 11:13
# Conflicts:
#	Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
@gjcairo gjcairo added the ⚠️ semver/major Breaks existing public API. label Jun 30, 2026
@gjcairo
gjcairo force-pushed the worktree-connection-lifecycle branch from d879cfb to 413211a Compare June 30, 2026 11:02
Comment on lines +72 to +80
let connectionLogger: Logger = {
var logger = rootLogger
let peer = context.remoteAddress.map { "\($0)" } ?? "unknown"
logger[metadataKey: "peer"] = .string(peer)
logger[metadataKey: "http"] = .string("\(context.httpVersion)")
return logger
}()
connectionLogger.info("connection accepted")
defer { connectionLogger.info("connection closed") }

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.

Let's use the new task local Logging APIs for this example. Actually the HTTP Server itself should set the task local logger and configure it correctly with metadata. This shouldn't be up to the user.

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.

what metadata should be included? In some applications it may not be desirable to log certain things like peer IP.

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.

The HTTP server should make a decision about the general useful metadata it wants to include. I would say things like HTTP version and peer IP are sensible. The rest the user can extract from the context and add.

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.

IMO, peer IP is sensitive and shouldn't be logged by default

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.

Can we keep the docs updates like this separate?

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.

Even when it's related to the change being introduced in the PR?

extension NIOHTTPServer {
/// Connection-specific information available during request handling.
/// The application-level HTTP version negotiated for a connection.
public enum HTTPVersion: Sendable, Hashable {

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.

This should also become part of HTTPAPIs

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.

@gjcairo gjcairo changed the title Worktree connection lifecycle Add connection lifecycle handling Jun 30, 2026

@ehaydenr ehaydenr 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.

This is looking good at a high level. Can we split it up into two PRs, though? I think CloseableConnection might make sense to have as a subsequent PR.

@gjcairo
gjcairo requested a review from ehaydenr July 6, 2026 09:56
@gjcairo gjcairo added 🆕 semver/minor Adds new public API. and removed ⚠️ semver/major Breaks existing public API. labels Jul 6, 2026
@gjcairo
gjcairo requested a review from FranzBusch July 6, 2026 09:56
@gjcairo

gjcairo commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

@ehaydenr split it out: #94

Comment thread Sources/NIOHTTPServer/NIOHTTPServer+Connection.swift
Comment thread Sources/NIOHTTPServer/NIOHTTPServer+Connection.swift Outdated
Comment thread Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
}
} catch {
self.logger.debug(
"Error tearing down HTTP/1.1 channel",

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.

executeThenClose doesn't throw if there was an error closing the channel. It throws the error that was caught in the body, so the "tearing down" part of the log statement isn't accurate here.

See https://github.com/apple/swift-nio/blob/b1f2dd18fcfd3f4b0a6d7ddc55c683c494b085a3/Sources/NIOCore/AsyncChannel/AsyncChannel.swift#L333-L372.

self.logger.debug(
"Error thrown by connection handler",
metadata: ["error": "\(error)"]
)

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

We should add a try? await connectionChannel.close() here.

Otherwise, the connection does not get cleaned up if an error is thrown before handleRequests is called.

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.

Edit: try? await connectionChannel.close() should be after the do-catch block so that we also clean up the connection if handleConnection did not call handleRequests.

/// derived.
public protocol PeerCertificate: RequestContext {
/// The peer's validated certificate chain, when available.
var peerCertificateChain: X509.ValidatedCertificateChain? { get async throws }

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.

Why is this async and throws? That seems very odd

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.

This was already its signature - I'm just introducing this capability. Look at NIOHTTPServer+ConnectionContext.swift:ConnectionContext/peerCertificateChain.
Based on how we're getting it, it has to be async throws.

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.

That is highly unfortunate since it should be synchronously present. Let's file an issue to track this since in the HTTPAPIs we should not make the property async or throws

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.

Why should it be synchronously present and not throw?

This is currently async because we're are calling get() on an ELF. We could just block if we need it to be sync.

We can't get around the throws unless we try?, but is hiding the errors desired?

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.

Because at the point where we hand the connection to the user we already have the TLS connection established. So we should be able to get the peer cert before we hand the connection to the user

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.

Sure. We can probably get it via sync operations. I'll create a follow up issue.

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.

#97

@gjcairo
gjcairo requested a review from FranzBusch July 14, 2026 14:53
@gjcairo
gjcairo requested a review from aryan-25 July 14, 2026 14:53
Comment thread Sources/NIOHTTPServer/NIOHTTPServerDefaultConnectionHandler.swift
@rafaelcepeda
rafaelcepeda merged commit cd75ec0 into main Jul 14, 2026
16 of 22 checks passed
@rafaelcepeda
rafaelcepeda deleted the worktree-connection-lifecycle branch July 14, 2026 17:37
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.

5 participants