Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
19 changes: 17 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let package = Package(
],
traits: [
.trait(name: "SwiftConfiguration"),
.trait(name: "ServiceLifecycle"),
.default(enabledTraits: ["SwiftConfiguration"]),
Copy link
Collaborator

Choose a reason for hiding this comment

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

It'd be nice if we could also enable the ServiceLifecycle trait by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The ServiceLifecycle trait is now no longer needed; NIOHTTPServer depends on swift-service-lifecycle by default.

],
dependencies: [
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,23 @@ 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).
To enable an additional trait on the package, update the package dependency:

```diff
.package(
url: "https://github.com/swift-server/swift-http-server.git",
from: "...",
+ traits: [.defaults, "ServiceLifecycle"]
)
```

Available traits:
- **`SwiftConfiguration`** (default): Enables initializing `NIOHTTPServerConfiguration` from a `swift-configuration`
`ConfigProvider`.
- **`ServiceLifecycle`** (opt-in): Enables `HTTPService`, which allows the server to be run with `ServiceGroup` from
`swift-service-lifecycle`, including support for graceful shutdown.
19 changes: 19 additions & 0 deletions Sources/HTTPServer/ServiceLifecycle/HTTPGracefulShutdown.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// 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,46 @@
//===----------------------------------------------------------------------===//
//
// 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 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.
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
) {
self.server = server
self.serverHandler = HTTPServerClosureRequestHandler(handler: serverHandler)
}
}
#endif // ServiceLifecycle
55 changes: 55 additions & 0 deletions Sources/HTTPServer/ServiceLifecycle/HTTPService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// 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 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

/// - Parameters:
/// - server: The underlying HTTPServer instance.
/// - serverHandler: The request handler that `server` will use.
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
}

/// 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()
}
)
}
}
#endif // ServiceLifecycle
15 changes: 15 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,17 @@ extension NIOHTTPServer {
case .hostAndPort(let host, let port):
let serverChannel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
.serverChannelInitializer { channel in
#if ServiceLifecycle
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
#else
channel.eventLoop.makeSucceededVoidFuture()
#endif // ServiceLifecycle
}
.bind(host: host, port: port) { channel in
self.setupHTTP1_1ConnectionChildChannel(
channel: channel,
Expand Down
81 changes: 57 additions & 24 deletions Sources/NIOHTTPServer/NIOHTTPServer+SecureUpgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import HTTPServer
import Logging
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOHTTP2
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
Expand All @@ -24,14 +25,18 @@ import NIOPosix
import NIOSSL
import X509

#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 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,24 @@ 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
#if ServiceLifecycle
channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.addHandler(
self.serverQuiescingHelper.makeServerChannelHandler(channel: channel)
)
}
#else
channel.eventLoop.makeSucceededVoidFuture()
#endif // ServiceLifecycle
}
.bind(host: host, port: port) { channel in
self.setupSecureUpgradeConnectionChildChannel(
channel: channel,
Expand All @@ -81,39 +97,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