Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 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`.
8 changes: 8 additions & 0 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 Down Expand Up @@ -43,6 +44,13 @@ extension NIOHTTPServer {
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 Down
74 changes: 50 additions & 24 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 @@ -31,7 +33,7 @@ extension NIOHTTPServer {
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.Configuration,
http2Configuration: NIOHTTPServerConfiguration.HTTP2,
verificationCallback: (@Sendable ([X509.Certificate]) async throws -> CertificateVerificationResult)? = nil
) async throws {
let serverChannel = try await self.setupSecureUpgradeServerChannel(
Expand All @@ -54,13 +56,20 @@ extension NIOHTTPServer {
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.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,
Expand All @@ -81,39 +90,56 @@ extension NIOHTTPServer {
channel: any Channel,
tlsConfiguration: TLSConfiguration,
asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration,
http2Configuration: NIOHTTP2Handler.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.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
)
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: { 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
http2ConnectionInitializer: { http2Channel in
http2Channel.eventLoop.makeCompletedFuture {
try http2Channel.pipeline.syncOperations.configureAsyncHTTP2Pipeline(
mode: .server,
connectionManagerConfiguration: .init(
maxIdleTime: nil,
maxAge: nil,
maxGraceTime: http2Configuration.gracefulShutdown.maxGraceTime,
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)))
}
}
)
}
Expand Down
21 changes: 19 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,11 +302,26 @@ 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:
/// - `maxGraceTimeSeconds` (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(maxGraceTime: config.int(forKey: "maxGraceTimeSeconds").map { .seconds(Int64($0)) })
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
extension NIOHTTPServerConfiguration.TransportSecurity {
fileprivate enum TransportSecurityKind: String {
Expand Down
56 changes: 29 additions & 27 deletions Sources/NIOHTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ public import Logging
import NIOCertificateReloading
import NIOConcurrencyHelpers
import NIOCore
import NIOExtras
import NIOHTTP1
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOHTTPTypesHTTP2
import NIOPosix
import NIOSSL
import ServiceLifecycle
import SwiftASN1
import Synchronization
import X509
Expand Down Expand Up @@ -85,6 +87,8 @@ public struct NIOHTTPServer: HTTPServer {
let logger: Logger
private let configuration: NIOHTTPServerConfiguration

let serverQuiescingHelper: ServerQuiescingHelper

var listeningAddressState: NIOLockedValueBox<State>

/// Task-local storage for connection-specific information accessible from request handlers.
Expand All @@ -106,6 +110,8 @@ public struct NIOHTTPServer: HTTPServer {
// TODO: If we allow users to pass in an event loop, use that instead of the singleton MTELG.
let eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup
self.listeningAddressState = .init(.idle(eventLoopGroup.any().makePromise()))

self.serverQuiescingHelper = .init(group: eventLoopGroup)
}

/// Starts an HTTP server with the specified request handler.
Expand Down Expand Up @@ -149,15 +155,16 @@ public struct NIOHTTPServer: HTTPServer {
public func serve(
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
) async throws {
defer {
switch self.listeningAddressState.withLockedValue({ $0.close() }) {
case .failPromise(let promise, let error):
promise.fail(error)
case .doNothing:
()
}
try await withTaskCancellationOrGracefulShutdownHandler {
try await self._serve(handler: handler)
} onCancelOrGracefulShutdown: {
self.close()
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems wrong to me. The behavior of a cancellation and graceful shutdown should be different. On cancellation we should force close all the channels on graceful shutdown we should quiesce

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've did some refactoring in order to get a reference to the underlying server channel in the serve method. It now looks like this:

let serverChannel = try await self.makeServerChannel()

return try await withTaskCancellationHandler {
    try await withGracefulShutdownHandler {
        try await self._serve(serverChannel: serverChannel, handler: handler)
    } onGracefulShutdown: {
        self.beginGracefulShutdown()
    }
} onCancel: {
    // Forcefully close down the server channel
    self.close(serverChannel: serverChannel)
}

}
}

private func _serve(
handler: some HTTPServerRequestHandler<RequestConcludingReader, ResponseConcludingWriter>
) async throws {
let asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration
switch self.configuration.backpressureStrategy.backing {
case .watermark(let low, let high):
Expand All @@ -176,10 +183,6 @@ public struct NIOHTTPServer: HTTPServer {
)

case .tls(let certificateChain, let privateKey):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: self.configuration.http2
)

let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) }
let privateKey = try NIOSSLPrivateKeySource(privateKey)

Expand All @@ -194,14 +197,10 @@ public struct NIOHTTPServer: HTTPServer {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config
http2Configuration: self.configuration.http2
)

case .reloadingTLS(let certificateReloader):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: configuration.http2
)

var tlsConfiguration: TLSConfiguration = try .makeServerConfiguration(
certificateReloader: certificateReloader
)
Expand All @@ -212,14 +211,10 @@ public struct NIOHTTPServer: HTTPServer {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config
http2Configuration: self.configuration.http2
)

case .mTLS(let certificateChain, let privateKey, let trustRoots, let verificationMode, let verificationCallback):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: configuration.http2
)

let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) }
let privateKey = try NIOSSLPrivateKeySource(privateKey)
let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots)
Expand All @@ -237,15 +232,11 @@ public struct NIOHTTPServer: HTTPServer {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config,
http2Configuration: self.configuration.http2,
verificationCallback: verificationCallback
)

case .reloadingMTLS(let certificateReloader, let trustRoots, let verificationMode, let verificationCallback):
let http2Config = NIOHTTP2Handler.Configuration(
httpServerHTTP2Configuration: configuration.http2
)

let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots)

var tlsConfiguration: TLSConfiguration = try .makeServerConfigurationWithMTLS(
Expand All @@ -260,7 +251,7 @@ public struct NIOHTTPServer: HTTPServer {
tlsConfiguration: tlsConfiguration,
handler: handler,
asyncChannelConfiguration: asyncChannelConfiguration,
http2Configuration: http2Config,
http2Configuration: self.configuration.http2,
verificationCallback: verificationCallback
)
}
Expand Down Expand Up @@ -345,6 +336,17 @@ public struct NIOHTTPServer: HTTPServer {
throw error
}
}

func close() {
switch self.listeningAddressState.withLockedValue({ $0.close() }) {
case .failPromise(let promise, let error):
promise.fail(error)
case .doNothing:
()
}

self.serverQuiescingHelper.initiateShutdown(promise: nil)
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
Expand Down
Loading
Loading