Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
21 changes: 18 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ let package = Package(
],
traits: [
.trait(name: "SwiftConfiguration"),
.default(enabledTraits: ["SwiftConfiguration"]),
.trait(name: "ServiceLifecycle"),
.default(enabledTraits: ["SwiftConfiguration", "ServiceLifecycle"]),
],
dependencies: [
.package(
Expand All @@ -55,8 +56,10 @@ 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"),
// TODO: Update dependency once PR is merged.
.package(url: "https://github.com/aryan-25/swift-nio-http2.git", branch: "server-connection-manager"),
.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.9.1"),
],
targets: [
.executableTarget(
Expand All @@ -78,6 +81,11 @@ let package = Package(
dependencies: [
"AsyncStreaming",
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(
name: "ServiceLifecycle",
package: "swift-service-lifecycle",
condition: .when(traits: ["ServiceLifecycle"])
),
],
swiftSettings: extraSettings
),
Expand All @@ -103,6 +111,11 @@ let package = Package(
package: "swift-configuration",
condition: .when(traits: ["SwiftConfiguration"])
),
.product(
name: "NIOExtras",
package: "swift-nio-extras",
condition: .when(traits: ["ServiceLifecycle"])
),
"HTTPServer",
],
swiftSettings: extraSettings
Expand All @@ -126,6 +139,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
18 changes: 18 additions & 0 deletions Sources/HTTPServer/ServiceLifecycle/HTTPGracefulShutdown.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A protocol for HTTP servers that support graceful shutdown.
public protocol GracefulShutdownService {
/// Initiates graceful shutdown of the HTTP server.
func beginGracefulShutdown()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if ServiceLifecycle
public import AsyncStreaming
public import HTTPTypes

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension HTTPService
where
Handler == HTTPServerClosureRequestHandler<
Server.RequestReader,
Server.RequestReader.Underlying,
Server.ResponseWriter,
Server.ResponseWriter.Underlying
>
{
/// - Parameters:
/// - server: The underlying HTTPServer instance.
/// - serverHandler: The request handler closure.
/// - gracefulShutdownHandler: A closure to execute upon graceful shutdown.
public init(
server: Server,
serverHandler:
nonisolated(nonsending) @Sendable @escaping (
_ request: HTTPRequest,
_ requestContext: HTTPRequestContext,
_ requestBodyAndTrailers: consuming sending Server.RequestReader,
_ responseSender: consuming sending HTTPResponseSender<Server.ResponseWriter>
) async throws -> Void,
gracefulShutdownHandler: @Sendable @escaping () -> Void = {}
) {
self.server = server
self.serverHandler = HTTPServerClosureRequestHandler(handler: serverHandler)
self.gracefulShutdownHandler = gracefulShutdownHandler
}
}
#endif // ServiceLifecycle
61 changes: 61 additions & 0 deletions Sources/HTTPServer/ServiceLifecycle/HTTPService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if ServiceLifecycle
public import ServiceLifecycle

/// A wrapper over HTTPServer that integrates with ServiceLifecycle.
@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
public struct HTTPService<
Server: HTTPServer & GracefulShutdownService,
Handler: HTTPServerRequestHandler
>: Service
where
Server.RequestReader == Handler.RequestReader,
Server.ResponseWriter == Handler.ResponseWriter
{
let server: Server
let serverHandler: Handler
let gracefulShutdownHandler: @Sendable () -> Void

/// - Parameters:
/// - server: The underlying HTTPServer instance.
/// - serverHandler: The request handler that `server` will use.
/// - onGracefulShutdown: A closure to execute upon graceful shutdown.
///
/// - Note: The `onGracefulShutdown` closure will be called *after* initiating graceful shutdown on `server`.
public init(
server: Server,
serverHandler: Handler,
onGracefulShutdown gracefulShutdownHandler: @Sendable @escaping () -> Void = {}
Copy link
Contributor

@FranzBusch FranzBusch Feb 11, 2026

Choose a reason for hiding this comment

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

Why do we need to let the user customize this? They can setup their own handler by wrapping this service if they wanted to.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. I hadn't thought about this. I've removed the onGracefulShutdown argument now.

) {
self.server = server
self.serverHandler = serverHandler
self.gracefulShutdownHandler = gracefulShutdownHandler
}

/// Runs the HTTP server and handles graceful shutdown when signaled.
public func run() async throws {
try await withGracefulShutdownHandler(
operation: {
try await self.server.serve(handler: self.serverHandler)
},
onGracefulShutdown: {
self.server.beginGracefulShutdown()
// Call the user-provided graceful shutdown handler
self.gracefulShutdownHandler()
}
)
}
}
#endif // ServiceLifecycle
13 changes: 13 additions & 0 deletions Sources/NIOHTTPServer/NIOHTTPServer+HTTP1_1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOPosix

#if ServiceLifecycle
import NIOExtras // For ServerQuiescingHelper, which is used for graceful shutdown.
#endif

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
func serveInsecureHTTP1_1(
Expand All @@ -43,6 +47,15 @@ extension NIOHTTPServer {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
#if ServiceLifecycle
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}
#endif // ServiceLifecycle
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
Expand Down
79 changes: 55 additions & 24 deletions Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ import NIOPosix
import NIOSSL
import X509

#if ServiceLifecycle
import NIOExtras // For ServerQuiescingHelper, which is used for graceful shutdown.
import NIOHTTP1
#endif

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer {
func serveSecureUpgrade(
bindTarget: NIOHTTPServerConfiguration.BindTarget,
tlsConfiguration: TLSConfiguration,
handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>,
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 +59,22 @@ 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)
#if ServiceLifecycle
.serverChannelInitializer { channel in
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
}
#endif // ServiceLifecycle
.bind(host: host, port: port) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
Expand All @@ -81,39 +95,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
29 changes: 29 additions & 0 deletions Sources/NIOHTTPServer/NIOHTTPServer+ServiceLifecycle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift HTTP Server open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if ServiceLifecycle
public import HTTPServer
import HTTPTypes
import Logging
import NIOExtras

@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *)
extension NIOHTTPServer: GracefulShutdownService {
/// Initiates graceful shutdown of the HTTP server.
public func beginGracefulShutdown() {
self.close()
self.serverQuiescingHelper.initiateShutdown(promise: nil)
}
}
#endif // ServiceLifecycle
Loading
Loading