Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4c6a241
Add support for swift-service-lifecycle
aryan-25 Feb 6, 2026
d9b1859
Merge branch 'main' into service-lifecycle
aryan-25 Feb 9, 2026
4f29521
Fix license headers
aryan-25 Feb 16, 2026
126ca49
Update tests
aryan-25 Feb 16, 2026
799ae5f
Remove `onGracefulShutdown` argument
aryan-25 Feb 16, 2026
d64751b
Formatting
aryan-25 Feb 16, 2026
54aa03b
Enclose ServiceLifecycle tests with `#if` trait guard
aryan-25 Feb 16, 2026
c07e575
Move `NIOHTTP1` import outside ServiceLifecycle guard
aryan-25 Feb 16, 2026
2eb4ec8
Disable `ServiceLifecycle` trait by default
aryan-25 Feb 16, 2026
4c4b00b
Add section for traits in README
aryan-25 Feb 16, 2026
a6e41d2
Formatting
aryan-25 Feb 16, 2026
94ea5d2
Guard ServiceLifecycle dependency in test target behind condition
aryan-25 Feb 16, 2026
ffdc5ca
Merge branch 'main' into service-lifecycle
aryan-25 Feb 18, 2026
29958fe
Remove changes to abstract `HTTPServer` API
aryan-25 Feb 18, 2026
53b0a01
Update availability
aryan-25 Feb 18, 2026
aca69bd
Merge branch 'main' into service-lifecycle
aryan-25 Feb 19, 2026
ea5a2da
Update README with new trait name
aryan-25 Feb 19, 2026
f6d076a
Update NIOHTTP2 dependency
aryan-25 Feb 19, 2026
321af08
Update HTTPServer dependency
aryan-25 Feb 19, 2026
2d57b9a
Formatting
aryan-25 Feb 19, 2026
17a0587
Remove `GracefulShutdownService` conformance
aryan-25 Feb 20, 2026
05a003e
Remove `ServiceLifecycle` trait; depend on `swift-service-lifecycle` …
aryan-25 Feb 20, 2026
91298cf
Support graceful shutdown in `serve(handler:)`
aryan-25 Feb 20, 2026
8aa2e2b
Update tests
aryan-25 Feb 20, 2026
e553a8f
Formatting
aryan-25 Feb 20, 2026
9d4d54a
Update `swift-service-lifecycle` dependency
aryan-25 Feb 20, 2026
2adab4a
Fix Package.swift
aryan-25 Feb 20, 2026
8f76939
Merge branch 'main' into service-lifecycle
aryan-25 Feb 20, 2026
8834692
Forcefully close upon server task cancellation; rename `maxGraceTime`…
aryan-25 Feb 25, 2026
cf189f5
Add test
aryan-25 Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let package = Package(
.default(enabledTraits: ["Configuration"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-http-api-proposal", branch: "main"),
.package(url: "https://github.com/apple/swift-http-api-proposal.git", branch: "main"),
.package(
url: "https://github.com/FranzBusch/swift-collections.git",
branch: "fb-async"
Expand All @@ -52,8 +52,9 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "2.92.2"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.36.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.30.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.40.0"),
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.10.0"),
],
targets: [
.executableTarget(
Expand Down Expand Up @@ -91,6 +92,7 @@ let package = Package(
package: "swift-configuration",
condition: .when(traits: ["Configuration"])
),
.product(name: "NIOExtras", package: "swift-nio-extras"),
.product(name: "HTTPServer", package: "swift-http-api-proposal"),
],
swiftSettings: extraSettings
Expand All @@ -99,6 +101,8 @@ let package = Package(
name: "NIOHTTPServerTests",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle"),
"NIOHTTPServer",
]
),
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ All feedback is welcome: please open issues!
## Getting started

To get started, please refer to the project's documentation and the Example located under `Sources`.

## Package traits

This package offers additional integrations you can enable using
[package traits](https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/addingdependencies#Packages-with-Traits).

Available traits:
- **`Configuration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration`
`ConfigProvider`.
51 changes: 23 additions & 28 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import HTTPServer
import NIOCore
import NIOEmbedded
import NIOExtras
import NIOHTTP1
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
Expand All @@ -23,26 +24,38 @@ import NIOPosix
@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServer {
func serveInsecureHTTP1_1(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
) async throws {
let serverChannel = try await self.setupHTTP1_1ServerChannel(
bindTarget: bindTarget,
asyncChannelConfiguration: asyncChannelConfiguration
)

try await _serveInsecureHTTP1_1(serverChannel: serverChannel, handler: handler)
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
for try await http1Channel in inbound {
group.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
}
}
}
}
}

private func setupHTTP1_1ServerChannel(
func setupHTTP1_1ServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
) async throws -> NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
Expand All @@ -69,22 +82,4 @@ extension NIOHTTPServer {
)
}
}

func _serveInsecureHTTP1_1(
serverChannel: NIOAsyncChannel<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>, Never>,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
) async throws {
try await withThrowingDiscardingTaskGroup { group in
try await serverChannel.executeThenClose { inbound in
for try await http1Channel in inbound {
group.addTask {
try await self.handleRequestChannel(
channel: http1Channel,
handler: handler
)
}
}
}
}
}
}
186 changes: 97 additions & 89 deletions Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import HTTPServer
import Logging
import NIOCore
import NIOEmbedded
import NIOExtras
import NIOHTTP1
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
Expand All @@ -26,100 +28,12 @@ import X509

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServer {
func serveSecureUpgrade(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil
) async throws {
let serverChannel = try await self.setupSecureUpgradeServerChannel(
bindTarget: bindTarget,
tlsConfiguration: tlsConfiguration,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Configuration,
verificationCallback: verificationCallback
)

try await self._serveSecureUpgrade(serverChannel: serverChannel, handler: handler)
}

typealias NegotiatedChannel = NIONegotiatedHTTPVersion<
NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>,
(any Channel, NIOHTTP2Handler.AsyncStreamMultiplexer<NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>>)
>

private func setupSecureUpgradeServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) async throws -> NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.bind(host: host, port: port) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
tlsConfiguration: tlsConfiguration,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Configuration,
verificationCallback: verificationCallback
)
}

try self.addressBound(serverChannel.channel.localAddress)

return serverChannel
}
}

func setupSecureUpgradeConnectionChildChannel(
channel: any Channel,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) -> EventLoopFuture<EventLoopFuture<NegotiatedChannel>> {
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.makeSSLServerHandler(tlsConfiguration, verificationCallback)
)
}.flatMap {
channel.configureAsyncHTTPServerPipeline(
http2Configuration: http2Configuration,
http1ConnectionInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: channel,
configuration: asyncChannelConfiguration
)
}
},
http2ConnectionInitializer: { channel in channel.eventLoop.makeCompletedFuture(.success(channel)) },
http2StreamInitializer: { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations
.addHandler(
HTTP2FramePayloadToHTTPServerCodec()
)

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: channel,
configuration: asyncChannelConfiguration
)
}
}
)
}
}

func _serveSecureUpgrade(
func serveSecureUpgrade(
serverChannel: NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never>,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
) async throws {
Expand Down Expand Up @@ -166,6 +80,100 @@ extension NIOHTTPServer {
}
}
}

func setupSecureUpgradeServerChannel(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTPServerConfiguration.HTTP2,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) async throws -> NIOAsyncChannel<EventLoopFuture<NegotiatedChannel>, Never> {
switch bindTarget.backing {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}
.bind(host: host, port: port) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
tlsConfiguration: tlsConfiguration,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Configuration,
verificationCallback: verificationCallback
)
}

try self.addressBound(serverChannel.channel.localAddress)

return serverChannel
}
}

func setupSecureUpgradeConnectionChildChannel(
channel: any Channel,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTPServerConfiguration.HTTP2,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)?
) -> EventLoopFuture<EventLoopFuture<NegotiatedChannel>> {
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.makeSSLServerHandler(tlsConfiguration, verificationCallback)
)
}.flatMap {
channel.configureHTTP2AsyncSecureUpgrade(
http1ConnectionInitializer: { http1Channel in
http1Channel.pipeline.configureHTTPServerPipeline().flatMap { _ in
http1Channel.eventLoop.makeCompletedFuture {
try http1Channel.pipeline.syncOperations.addHandler(HTTP1ToHTTPServerCodec(secure: true))

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: http1Channel,
configuration: asyncChannelConfiguration
)
}
}
},
http2ConnectionInitializer: { http2Channel in
http2Channel.eventLoop.makeCompletedFuture {
try http2Channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
mode: .server,
connectionManagerConfiguration: .init(
maxIdleTime: nil,
maxAge: nil,
maxGraceTime: http2Configuration.gracefulShutdown.maximumGracefulShutdownDuration
.map { TimeAmount($0) },
keepalive: nil
),
http2HandlerConfiguration: .init(httpServerHTTP2Configuration: http2Configuration),
streamInitializer: { http2StreamChannel in
http2StreamChannel.eventLoop.makeCompletedFuture {
try http2StreamChannel.pipeline.syncOperations
.addHandler(
HTTP2FramePayloadToHTTPServerCodec()
)

return try NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>(
wrappingChannelSynchronously: http2StreamChannel,
configuration: asyncChannelConfiguration
)
}
}
)
}
.flatMap { multiplexer in
http2Channel.eventLoop.makeCompletedFuture(.success((http2Channel, multiplexer)))
}
}
)
}
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
Expand Down
23 changes: 21 additions & 2 deletions Sources/NIOHTTPServer/NIOHTTPServer+SwiftConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

#if Configuration
public import Configuration
import NIOCore
import NIOCertificateReloading
import NIOHTTP2
import SwiftASN1
public import X509

Expand Down Expand Up @@ -280,7 +282,7 @@ extension NIOHTTPServerConfiguration.HTTP2 {
/// Initialize a HTTP/2 configuration from a config reader.
///
/// ## Configuration keys:
/// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection.
/// - `maxFrameSize` (int, optional, default: 2^14): The maximum frame size to be used in an HTTP/2 connection.
/// - `targetWindowSize` (int, optional, default: 2^16 - 1): The target window size to be used in an HTTP/2
/// connection.
/// - `maxConcurrentStreams` (int, optional, default: 100): The maximum number of concurrent streams in an HTTP/2
Expand All @@ -300,7 +302,24 @@ extension NIOHTTPServerConfiguration.HTTP2 {
/// The default value, ``NIOHTTPServerConfiguration.HTTP2.DEFAULT_TARGET_WINDOW_SIZE``, is `nil`. However,
/// we can only specify a non-nil `default` argument to `config.int(...)`. But `config.int(...)` already
/// defaults to `nil` if it can't find the `"maxConcurrentStreams"` key, so that works for us.
maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams")
maxConcurrentStreams: config.int(forKey: "maxConcurrentStreams"),
gracefulShutdown: .init(config: config)
)
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.HTTP2.GracefulShutdownConfiguration {
/// Initialize a HTTP/2 graceful shutdown configuration from a config reader.
///
/// ## Configuration keys:
/// - `maximumGracefulShutdownDuration` (int, optional, default: nil): The maximum amount of time (in seconds) that
/// the connection has to close gracefully.
///
/// - Parameter config: The configuration reader.
public init(config: ConfigSnapshotReader) {
self.init(
maximumGracefulShutdownDuration: config.int(forKey: "maximumGracefulShutdownDuration").map { .seconds($0) }
)
}
}
Expand Down
Loading
Loading