Skip to content

Commit 8270dac

Browse files
authored
Add support for swift-service-lifecycle (#55)
Motivation: The [`swift-service-lifecycle`](https://github.com/swift-server/swift-service-lifecycle) library is commonly used for managing service lifetimes. We should adopt `swift-service-lifecycle` for `NIOHTTPServer`. Modifications: - Updated the child channel initializers for HTTP/1.1 and Secure Upgrade in `NIOHTTPServer` to use [`swift-nio-extra`'s `ServerQuiescingHelper`](https://swiftpackageindex.com/apple/swift-nio-extras/main/documentation/nioextras/serverquiescinghelper). The `ServerQuiescingHelper` is added to the channel pipeline, and propagates a `ChannelShouldQuiesceEvent` upon graceful shutdown being triggered. - For HTTP/1.1, the [`HTTPServerPipelineHandler`](https://swiftpackageindex.com/apple/swift-nio/2.90.0/documentation/niohttp1/httpserverpipelinehandler) handler, which is contained in the pipeline, already handles `ChannelShouldQuiesceEvent` events. - For HTTP/2, a [`NIOHTTP2ServerConnectionManager`](https://swiftpackageindex.com/apple/swift-nio-http2/main/documentation/niohttp2/niohttp2serverconnectionmanagementhandler) is added to the pipeline. This connection manager reacts to a `ChannelShouldQuiesceEvent` and gracefully shuts down HTTP/2 connections. - Updated `NIOHTTPServer`'s `serve(handler:)` to handle graceful shutdown (using `swift-service-lifecycle`'s [`withTaskCancellationOrGracefulShutdownHandler` method](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/2.9.1/documentation/servicelifecycle/withtaskcancellationorgracefulshutdownhandler(isolation:operation:oncancelorgracefulshutdown:))). Result: Support for `swift-service-lifecycle` added.
1 parent 4af762f commit 8270dac

13 files changed

+597
-183
lines changed

Package.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ let package = Package(
4040
.default(enabledTraits: ["Configuration"]),
4141
],
4242
dependencies: [
43-
.package(url: "https://github.com/apple/swift-http-api-proposal", branch: "main"),
43+
.package(url: "https://github.com/apple/swift-http-api-proposal.git", branch: "main"),
4444
.package(
4545
url: "https://github.com/FranzBusch/swift-collections.git",
4646
branch: "fb-async"
@@ -52,8 +52,9 @@ let package = Package(
5252
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
5353
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.36.0"),
5454
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.30.0"),
55-
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"),
56-
.package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"),
55+
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.40.0"),
56+
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"),
57+
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.10.0"),
5758
],
5859
targets: [
5960
.executableTarget(
@@ -91,6 +92,7 @@ let package = Package(
9192
package: "swift-configuration",
9293
condition: .when(traits: ["Configuration"])
9394
),
95+
.product(name: "NIOExtras", package: "swift-nio-extras"),
9496
.product(name: "HTTPServer", package: "swift-http-api-proposal"),
9597
],
9698
swiftSettings: extraSettings
@@ -99,6 +101,8 @@ let package = Package(
99101
name: "NIOHTTPServerTests",
100102
dependencies: [
101103
.product(name: "Logging", package: "swift-log"),
104+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
105+
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle"),
102106
"NIOHTTPServer",
103107
]
104108
),

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ All feedback is welcome: please open issues!
1212
## Getting started
1313

1414
To get started, please refer to the project's documentation and the Example located under `Sources`.
15+
16+
## Package traits
17+
18+
This package offers additional integrations you can enable using
19+
[package traits](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/addingdependencies#Packages-with-Traits).
20+
21+
Available traits:
22+
- **`Configuration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration`
23+
`ConfigProvider`.

Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import HTTPServer
1616
import NIOCore
1717
import NIOEmbedded
18+
import NIOExtras
1819
import NIOHTTP1
1920
import NIOHTTPTypes
2021
import NIOHTTPTypesHTTP1
@@ -23,26 +24,38 @@ import NIOPosix
2324
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
2425
extension NIOHTTPServer {
2526
func serveInsecureHTTP1_1(
26-
bindTarget: NIOHTTPServerConfiguration.BindTarget,
27-
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
28-
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
27+
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
28+
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
2929
) async throws {
30-
let serverChannel = try await self.setupHTTP1_1ServerChannel(
31-
bindTarget: bindTarget,
32-
asyncChannelConfiguration: asyncChannelConfiguration
33-
)
34-
35-
try await _serveInsecureHTTP1_1(serverChannel: serverChannel, handler: handler)
30+
try await withThrowingDiscardingTaskGroup { group in
31+
try await serverChannel.executeThenClose { inbound in
32+
for try await http1Channel in inbound {
33+
group.addTask {
34+
try await self.handleRequestChannel(
35+
channel: http1Channel,
36+
handler: handler
37+
)
38+
}
39+
}
40+
}
41+
}
3642
}
3743

38-
private func setupHTTP1_1ServerChannel(
44+
func setupHTTP1_1ServerChannel(
3945
bindTarget: NIOHTTPServerConfiguration.BindTarget,
4046
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
4147
) async throws -> NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never> {
4248
switch bindTarget.backing {
4349
case .hostAndPort(let host, let port):
4450
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
4551
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
52+
.serverChannelInitializer { channel in
53+
channel.eventLoop.makeCompletedFuture {
54+
try channel.pipeline.syncOperations.addHandler(
55+
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
56+
)
57+
}
58+
}
4659
.bind(host: host, port: port) { channel in
4760
self.setupHTTP1_1ConnectionChildChannel(
4861
channel: channel,
@@ -69,22 +82,4 @@ extension NIOHTTPServer {
6982
)
7083
}
7184
}
72-
73-
func _serveInsecureHTTP1_1(
74-
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
75-
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
76-
) async throws {
77-
try await withThrowingDiscardingTaskGroup { group in
78-
try await serverChannel.executeThenClose { inbound in
79-
for try await http1Channel in inbound {
80-
group.addTask {
81-
try await self.handleRequestChannel(
82-
channel: http1Channel,
83-
handler: handler
84-
)
85-
}
86-
}
87-
}
88-
}
89-
}
9085
}

Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift

Lines changed: 97 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import HTTPServer
1616
import Logging
1717
import NIOCore
1818
import NIOEmbedded
19+
import NIOExtras
20+
import NIOHTTP1
1921
import NIOHTTP2
2022
import NIOHTTPTypes
2123
import NIOHTTPTypesHTTP1
@@ -26,100 +28,12 @@ import X509
2628

2729
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
2830
extension NIOHTTPServer {
29-
func serveSecureUpgrade(
30-
bindTarget: NIOHTTPServerConfiguration.BindTarget,
31-
tlsConfiguration: TLSConfiguration,
32-
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
33-
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
34-
http2Configuration: NIOHTTP2Handler.Configuration,
35-
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil
36-
) async throws {
37-
let serverChannel = try await self.setupSecureUpgradeServerChannel(
38-
bindTarget: bindTarget,
39-
tlsConfiguration: tlsConfiguration,
40-
asyncChannelConfiguration: asyncChannelConfiguration,
41-
http2Configuration: http2Configuration,
42-
verificationCallback: verificationCallback
43-
)
44-
45-
try await self._serveSecureUpgrade(serverChannel: serverChannel, handler: handler)
46-
}
47-
4831
typealias NegotiatedChannel = NIONegotiatedHTTPVersion<
4932
NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
5033
(any Channel, NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>>)
5134
>
5235

53-
private func setupSecureUpgradeServerChannel(
54-
bindTarget: NIOHTTPServerConfiguration.BindTarget,
55-
tlsConfiguration: TLSConfiguration,
56-
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
57-
http2Configuration: NIOHTTP2Handler.Configuration,
58-
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
59-
) async throws -> NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never> {
60-
switch bindTarget.backing {
61-
case .hostAndPort(let host, let port):
62-
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
63-
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
64-
.bind(host: host, port: port) { channel in
65-
self.setupSecureUpgradeConnectionChildChannel(
66-
channel: channel,
67-
tlsConfiguration: tlsConfiguration,
68-
asyncChannelConfiguration: asyncChannelConfiguration,
69-
http2Configuration: http2Configuration,
70-
verificationCallback: verificationCallback
71-
)
72-
}
73-
74-
try self.addressBound(serverChannel.channel.localAddress)
75-
76-
return serverChannel
77-
}
78-
}
79-
80-
func setupSecureUpgradeConnectionChildChannel(
81-
channel: any Channel,
82-
tlsConfiguration: TLSConfiguration,
83-
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
84-
http2Configuration: NIOHTTP2Handler.Configuration,
85-
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
86-
) -> EventLoopFuture<EventLoopFuture<NegotiatedChannel>> {
87-
channel.eventLoop.makeCompletedFuture {
88-
try channel.pipeline.syncOperations.addHandler(
89-
self.makeSSLServerHandler(tlsConfiguration, verificationCallback)
90-
)
91-
}.flatMap {
92-
channel.configureAsyncHTTPServerPipeline(
93-
http2Configuration: http2Configuration,
94-
http1ConnectionInitializer: { channel in
95-
channel.eventLoop.makeCompletedFuture {
96-
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))
97-
98-
return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
99-
wrappingChannelSynchronously: channel,
100-
configuration: asyncChannelConfiguration
101-
)
102-
}
103-
},
104-
http2ConnectionInitializer: { channel in channel.eventLoop.makeCompletedFuture(.success(channel)) },
105-
http2StreamInitializer: { channel in
106-
channel.eventLoop.makeCompletedFuture {
107-
try channel.pipeline.syncOperations
108-
.addHandler(
109-
HTTP2FramePayloadToHTTPServerCodec()
110-
)
111-
112-
return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
113-
wrappingChannelSynchronously: channel,
114-
configuration: asyncChannelConfiguration
115-
)
116-
}
117-
}
118-
)
119-
}
120-
}
121-
122-
func _serveSecureUpgrade(
36+
func serveSecureUpgrade(
12337
serverChannel: NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never>,
12438
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
12539
) async throws {
@@ -166,6 +80,100 @@ extension NIOHTTPServer {
16680
}
16781
}
16882
}
83+
84+
func setupSecureUpgradeServerChannel(
85+
bindTarget: NIOHTTPServerConfiguration.BindTarget,
86+
tlsConfiguration: TLSConfiguration,
87+
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
88+
http2Configuration: NIOHTTPServerConfiguration.HTTP2,
89+
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
90+
) async throws -> NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never> {
91+
switch bindTarget.backing {
92+
case .hostAndPort(let host, let port):
93+
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
94+
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
95+
.serverChannelInitializer { channel in
96+
channel.eventLoop.makeCompletedFuture {
97+
try channel.pipeline.syncOperations.addHandler(
98+
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
99+
)
100+
}
101+
}
102+
.bind(host: host, port: port) { channel in
103+
self.setupSecureUpgradeConnectionChildChannel(
104+
channel: channel,
105+
tlsConfiguration: tlsConfiguration,
106+
asyncChannelConfiguration: asyncChannelConfiguration,
107+
http2Configuration: http2Configuration,
108+
verificationCallback: verificationCallback
109+
)
110+
}
111+
112+
try self.addressBound(serverChannel.channel.localAddress)
113+
114+
return serverChannel
115+
}
116+
}
117+
118+
func setupSecureUpgradeConnectionChildChannel(
119+
channel: any Channel,
120+
tlsConfiguration: TLSConfiguration,
121+
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
122+
http2Configuration: NIOHTTPServerConfiguration.HTTP2,
123+
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
124+
) -> EventLoopFuture<EventLoopFuture<NegotiatedChannel>> {
125+
channel.eventLoop.makeCompletedFuture {
126+
try channel.pipeline.syncOperations.addHandler(
127+
self.makeSSLServerHandler(tlsConfiguration, verificationCallback)
128+
)
129+
}.flatMap {
130+
channel.configureHTTP2AsyncSecureUpgrade(
131+
http1ConnectionInitializer: { http1Channel in
132+
http1Channel.pipeline.configureHTTPServerPipeline().flatMap { _ in
133+
http1Channel.eventLoop.makeCompletedFuture {
134+
try http1Channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))
135+
136+
return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
137+
wrappingChannelSynchronously: http1Channel,
138+
configuration: asyncChannelConfiguration
139+
)
140+
}
141+
}
142+
},
143+
http2ConnectionInitializer: { http2Channel in
144+
http2Channel.eventLoop.makeCompletedFuture {
145+
try http2Channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
146+
mode: .server,
147+
connectionManagerConfiguration: .init(
148+
maxIdleTime: nil,
149+
maxAge: nil,
150+
maxGraceTime: http2Configuration.gracefulShutdown.maximumGracefulShutdownDuration
151+
.map { TimeAmount($0) },
152+
keepalive: nil
153+
),
154+
http2HandlerConfiguration: .init(httpServerHTTP2Configuration: http2Configuration),
155+
streamInitializer: { http2StreamChannel in
156+
http2StreamChannel.eventLoop.makeCompletedFuture {
157+
try http2StreamChannel.pipeline.syncOperations
158+
.addHandler(
159+
HTTP2FramePayloadToHTTPServerCodec()
160+
)
161+
162+
return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
163+
wrappingChannelSynchronously: http2StreamChannel,
164+
configuration: asyncChannelConfiguration
165+
)
166+
}
167+
}
168+
)
169+
}
170+
.flatMap { multiplexer in
171+
http2Channel.eventLoop.makeCompletedFuture(.success((http2Channel, multiplexer)))
172+
}
173+
}
174+
)
175+
}
176+
}
169177
}
170178

171179
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)

Sources/NIOHTTPServer/NIOHTTPServer+SwiftConfiguration.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
#if Configuration
1616
public import Configuration
17+
import NIOCore
1718
import NIOCertificateReloading
19+
import NIOHTTP2
1820
import SwiftASN1
1921
public import X509
2022

@@ -280,7 +282,7 @@ extension NIOHTTPServerConfiguration.HTTP2 {
280282
/// Initialize a HTTP/2 configuration from a config reader.
281283
///
282284
/// ## Configuration keys:
283-
/// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection.
285+
/// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection.
284286
/// - `targetWindowSize` (int, optional, default: 2^16 - 1): The target window size to be used in an HTTP/2
285287
/// connection.
286288
/// - `maxConcurrentStreams` (int, optional, default: 100): The maximum number of concurrent streams in an HTTP/2
@@ -300,7 +302,24 @@ extension NIOHTTPServerConfiguration.HTTP2 {
300302
/// The default value, ``NIOHTTPServerConfiguration.HTTP2.DEFAULT_TARGET_WINDOW_SIZE``, is `nil`. However,
301303
/// we can only specify a non-nil `default` argument to `config.int(...)`. But `config.int(...)` already
302304
/// defaults to `nil` if it can't find the `"maxConcurrentStreams"` key, so that works for us.
303-
maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams")
305+
maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams"),
306+
gracefulShutdown: .init(config: config)
307+
)
308+
}
309+
}
310+
311+
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
312+
extension NIOHTTPServerConfiguration.HTTP2.GracefulShutdownConfiguration {
313+
/// Initialize a HTTP/2 graceful shutdown configuration from a config reader.
314+
///
315+
/// ## Configuration keys:
316+
/// - `maximumGracefulShutdownDuration` (int, optional, default: nil): The maximum amount of time (in seconds) that
317+
/// the connection has to close gracefully.
318+
///
319+
/// - Parameter config: The configuration reader.
320+
public init(config: ConfigSnapshotReader) {
321+
self.init(
322+
maximumGracefulShutdownDuration: config.int(forKey: "maximumGracefulShutdownDuration").map { .seconds($0) }
304323
)
305324
}
306325
}

0 commit comments

Comments
 (0)