-
Notifications
You must be signed in to change notification settings - Fork 0
Add server listening address property in NIOHTTPServer #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift HTTP Server open source project | ||
| // | ||
| // Copyright (c) 2025 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import NIOConcurrencyHelpers | ||
| import NIOCore | ||
| import NIOPosix | ||
|
|
||
| enum ListeningAddressError: CustomStringConvertible, Error { | ||
| case addressOrPortNotAvailable | ||
| case unsupportedAddressType | ||
| case serverClosed | ||
|
|
||
| var description: String { | ||
| switch self { | ||
| case .addressOrPortNotAvailable: | ||
| return "Unable to retrieve the bound address or port from the underlying socket" | ||
| case .unsupportedAddressType: | ||
| return "Unsupported address type: only IPv4 and IPv6 are supported" | ||
| case .serverClosed: | ||
| return """ | ||
| There is no listening address bound for this server: there may have been an error which caused the server to close, or it may have shut down. | ||
| """ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) | ||
| extension NIOHTTPServer { | ||
| func addressBound(_ address: NIOCore.SocketAddress?) throws { | ||
| switch self.listeningAddressState.withLockedValue({ $0.addressBound(address) }) { | ||
| case .succeedPromise(let promise, let boundAddress): | ||
| promise.succeed(boundAddress) | ||
| case .failPromise(let promise, let error): | ||
| promise.fail(error) | ||
| } | ||
| } | ||
|
|
||
| /// The address the server is listening from. | ||
| /// | ||
| /// It is an `async` property because it will only return once the address has been successfully bound. | ||
| /// | ||
| /// - Throws: An error will be thrown if the address could not be bound or is not bound any longer because the | ||
| /// server isn't listening anymore. | ||
| public var listeningAddress: SocketAddress { | ||
| get async throws { | ||
| try await self.listeningAddressState | ||
| .withLockedValue { try $0.listeningAddressFuture } | ||
| .get() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) | ||
| extension NIOHTTPServer { | ||
| enum State { | ||
| case idle(EventLoopPromise<SocketAddress>) | ||
| case listening(EventLoopFuture<SocketAddress>) | ||
| case closedOrInvalidAddress(ListeningAddressError) | ||
|
|
||
| var listeningAddressFuture: EventLoopFuture<SocketAddress> { | ||
| get throws { | ||
| switch self { | ||
| case .idle(let eventLoopPromise): | ||
| return eventLoopPromise.futureResult | ||
| case .listening(let eventLoopFuture): | ||
| return eventLoopFuture | ||
| case .closedOrInvalidAddress(let error): | ||
| throw error | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum OnBound { | ||
| case succeedPromise(_ promise: EventLoopPromise<SocketAddress>, address: SocketAddress) | ||
| case failPromise(_ promise: EventLoopPromise<SocketAddress>, error: ListeningAddressError) | ||
| } | ||
|
|
||
| mutating func addressBound(_ address: NIOCore.SocketAddress?) -> OnBound { | ||
| switch self { | ||
| case .idle(let listeningAddressPromise): | ||
| do { | ||
| let socketAddress = try SocketAddress(address) | ||
| self = .listening(listeningAddressPromise.futureResult) | ||
| return .succeedPromise(listeningAddressPromise, address: socketAddress) | ||
| } catch { | ||
| self = .closedOrInvalidAddress(error) | ||
| return .failPromise(listeningAddressPromise, error: error) | ||
| } | ||
|
|
||
| case .listening, .closedOrInvalidAddress: | ||
| fatalError("Invalid state: addressBound should only be called once and when in idle state") | ||
| } | ||
| } | ||
|
|
||
| enum OnClose { | ||
| case failPromise(_ promise: EventLoopPromise<SocketAddress>, error: ListeningAddressError) | ||
| case doNothing | ||
| } | ||
|
|
||
| mutating func close() -> OnClose { | ||
| switch self { | ||
| case .idle(let listeningAddressPromise): | ||
| self = .closedOrInvalidAddress(.serverClosed) | ||
| return .failPromise(listeningAddressPromise, error: .serverClosed) | ||
|
|
||
| case .listening: | ||
| self = .closedOrInvalidAddress(.serverClosed) | ||
| return .doNothing | ||
|
|
||
| case .closedOrInvalidAddress: | ||
| return .doNothing | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension SocketAddress { | ||
| fileprivate init(_ address: NIOCore.SocketAddress?) throws(ListeningAddressError) { | ||
| guard let address, let port = address.port else { | ||
| throw ListeningAddressError.addressOrPortNotAvailable | ||
| } | ||
|
|
||
| switch address { | ||
| case .v4(let ipv4Address): | ||
| self.init(base: .ipv4(.init(host: ipv4Address.host, port: port))) | ||
| case .v6(let ipv6Address): | ||
| self.init(base: .ipv6(.init(host: ipv6Address.host, port: port))) | ||
| case .unixDomainSocket: | ||
| throw ListeningAddressError.unsupportedAddressType | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift HTTP Server open source project | ||
| // | ||
| // Copyright (c) 2025 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import NIOCore | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need the NIO import here right?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I missed this. Fixed in #34. |
||
|
|
||
| /// Represents an IPv4 address. | ||
| public struct IPv4: Hashable, Sendable { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am a bit hesitant to add these types to this package. This is probably something that is generally useful and might require some API design on the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I don't think this should be part of the NIO implementation only. So yes it would make sense to move them to the other repo. Would you be okay with having these here for now, and I can open a PR on the other repo as well? Then once we land them there we can remove them from here alongside the other abstract pieces. Also: would
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it is okay to land them here for now but let's file an issue on the other repo. Ideally
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| /// The resolved host address. | ||
| public var host: String | ||
| /// The port to connect to. | ||
| public var port: Int | ||
|
|
||
| /// Creates a new IPv4 address. | ||
| /// | ||
| /// - Parameters: | ||
| /// - host: Resolved host address. | ||
| /// - port: Port to connect to. | ||
| public init(host: String, port: Int) { | ||
| self.host = host | ||
| self.port = port | ||
| } | ||
| } | ||
|
|
||
| /// Represents an IPv6 address. | ||
| public struct IPv6: Hashable, Sendable { | ||
| /// The resolved host address. | ||
| public var host: String | ||
| /// The port to connect to. | ||
| public var port: Int | ||
|
|
||
| /// Creates a new IPv6 address. | ||
| /// | ||
| /// - Parameters: | ||
| /// - host: Resolved host address. | ||
| /// - port: Port to connect to. | ||
| public init(host: String, port: Int) { | ||
| self.host = host | ||
| self.port = port | ||
| } | ||
| } | ||
|
|
||
| /// An address to which a socket may connect or bind to. | ||
| public struct SocketAddress: Hashable, Sendable { | ||
| enum Base: Hashable, Sendable { | ||
| case ipv4(IPv4) | ||
| case ipv6(IPv6) | ||
| } | ||
|
|
||
| let base: Base | ||
|
|
||
| /// Creates an IPv4 socket address. | ||
| public static func ipv4(host: String, port: Int) -> Self { | ||
| Self(base: .ipv4(.init(host: host, port: port))) | ||
| } | ||
|
|
||
| /// Creates an IPv6 socket address. | ||
| public static func ipv6(host: String, port: Int) -> Self { | ||
| Self(base: .ipv6(.init(host: host, port: port))) | ||
| } | ||
|
|
||
| /// Returns the address as an IPv4 address, if possible. | ||
| public var ipv4: IPv4? { | ||
| guard case .ipv4(let address) = self.base else { | ||
| return nil | ||
| } | ||
|
|
||
| return address | ||
| } | ||
|
|
||
| /// Returns the address as an IPv6 address, if possible. | ||
| public var ipv6: IPv6? { | ||
| guard case .ipv6(let address) = self.base else { | ||
| return nil | ||
| } | ||
|
|
||
| return address | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift HTTP Server open source project | ||
| // | ||
| // Copyright (c) 2025 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 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import HTTPTypes | ||
| import Logging | ||
| import NIOCore | ||
| import NIOHTTP1 | ||
| import NIOHTTPTypes | ||
| import NIOPosix | ||
| import Testing | ||
|
|
||
| @testable import HTTPServer | ||
|
|
||
| #if canImport(Dispatch) | ||
| import Dispatch | ||
| #endif | ||
|
|
||
| @Suite | ||
| struct NIOHTTPServerTests { | ||
| @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) | ||
| @Test("Obtain the listening address correctly") | ||
| func testListeningAddress() async throws { | ||
| let server = NIOHTTPServer( | ||
| logger: Logger(label: "Test"), | ||
| configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 1234)) | ||
| ) | ||
|
|
||
| try await withThrowingTaskGroup { group in | ||
| group.addTask { | ||
| try await server.serve { _, _, _, _ in } | ||
| } | ||
|
|
||
| let serverAddress = try await server.listeningAddress | ||
|
|
||
| let address = try #require(serverAddress.ipv4) | ||
| #expect(address.host == "127.0.0.1") | ||
| #expect(address.port == 1234) | ||
|
|
||
| group.cancelAll() | ||
| } | ||
|
|
||
| // Now that the server has shut down, try obtaining the listening address. This should result in an error. | ||
| await #expect(throws: ListeningAddressError.serverClosed) { | ||
| try await server.listeningAddress | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.