Skip to content

Commit b65b663

Browse files
Refine listening SocketAddress construction and add tests
Motivation: SocketAddress.init(_:) folded address, port and pathname lookups into a single tuple switch with coarse error cases, making failure reasons ambiguous and leaving the mapping untested. Modifications: - Replace addressOrPortNotAvailable/unsupportedAddressType with the more precise addressNotAvailable and portNotAvailable cases - Rebuild init(_:) around per-field throwing accessors and an exhaustive switch over the address kind - Switch the NIOClient test helper over SocketAddress.base instead of the optional host/port/path accessors - Add SocketAddressTests covering the IPv4, IPv6, UDS and nil mappings Result: Listening-address construction has precise error reasons and unit-test coverage; behaviour is unchanged.
1 parent 07b668d commit b65b663

3 files changed

Lines changed: 125 additions & 23 deletions

File tree

Sources/NIOHTTPServer/NIOHTTPServer+ListeningAddress.swift

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,20 @@
1515
import NIOConcurrencyHelpers
1616
import NIOCore
1717
import NIOPosix
18+
import System
1819

1920
enum ListeningAddressError: CustomStringConvertible, Error {
20-
case addressOrPortNotAvailable
21-
case unsupportedAddressType
21+
case addressNotAvailable
22+
case portNotAvailable
2223
case serverClosed
2324
case pathnameNotAvailable
2425

2526
var description: String {
2627
switch self {
27-
case .addressOrPortNotAvailable:
28-
return "Unable to retrieve the bound address or port from the underlying socket"
29-
case .unsupportedAddressType:
30-
return "Unsupported address type: only IPv4 and IPv6 are supported"
28+
case .addressNotAvailable:
29+
return "Unable to retrieve the bound address from the underlying socket"
30+
case .portNotAvailable:
31+
return "Unable to retrieve the bound port from the underlying socket"
3132
case .serverClosed:
3233
return """
3334
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.
@@ -138,24 +139,38 @@ extension NIOHTTPServer {
138139
extension NIOHTTPServer.SocketAddress {
139140
init(_ address: NIOCore.SocketAddress?) throws(ListeningAddressError) {
140141
guard let address else {
141-
throw .addressOrPortNotAvailable
142+
throw .addressNotAvailable
142143
}
143144

144-
let base: Base = switch (address, address.port, address.pathname) {
145-
case (.v4(let ipv4Address), .some(let port), _):
146-
.ipv4(.init(host: ipv4Address.host, port: port))
145+
var port: Int {
146+
get throws(ListeningAddressError) {
147+
guard let port = address.port else {
148+
throw .portNotAvailable
149+
}
150+
return port
151+
}
152+
}
153+
154+
var pathname: String {
155+
get throws(ListeningAddressError) {
156+
guard let pathname = address.pathname else {
157+
throw .pathnameNotAvailable
158+
}
159+
return pathname
160+
}
161+
}
147162

148-
case (.v6(let ipv6Address), .some(let port), _):
149-
.ipv6(.init(host: ipv6Address.host, port: port))
163+
let base: NIOHTTPServer.SocketAddress.Base
150164

151-
case (.unixDomainSocket, _, .some(let path)):
152-
.unixDomainSocket(path: path)
165+
switch address {
166+
case .v4(let ipv4Address):
167+
base = try .ipv4(.init(host: ipv4Address.host, port: port))
153168

154-
case (.v4, .none, _), (.v6, .none, _):
155-
throw .addressOrPortNotAvailable
169+
case .v6(let ipv6Address):
170+
base = try .ipv6(.init(host: ipv6Address.host, port: port))
156171

157-
case (.unixDomainSocket, _, .none):
158-
throw .pathnameNotAvailable
172+
case .unixDomainSocket(_):
173+
base = try .unixDomainSocket(path: pathname)
159174
}
160175

161176
self.init(base: base)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift HTTP Server open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift HTTP Server project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift HTTP Server project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import Testing
17+
18+
@testable import NIOHTTPServer
19+
20+
@Suite
21+
struct SocketAddressTests {
22+
@available(anyAppleOS 26.0, *)
23+
@Test("A nil NIO address throws addressNotAvailable")
24+
func testNilAddressThrows() throws {
25+
#expect(throws: ListeningAddressError.addressNotAvailable) {
26+
_ = try NIOHTTPServer.SocketAddress(nil as NIOCore.SocketAddress?)
27+
}
28+
}
29+
30+
@available(anyAppleOS 26.0, *)
31+
@Test("An IPv4 NIO address maps to the ipv4 base")
32+
func testIPv4Address() throws {
33+
// `makeAddressResolvingHost` populates the address' `host` field the way a real bound
34+
// `localAddress` does; `SocketAddress(ipAddress:port:)` would leave `host` empty.
35+
let nioAddress = try NIOCore.SocketAddress.makeAddressResolvingHost("127.0.0.1", port: 8080)
36+
let address = try NIOHTTPServer.SocketAddress(nioAddress)
37+
38+
let ipv4 = try #require(address.ipv4)
39+
#expect(ipv4.host == "127.0.0.1")
40+
#expect(ipv4.port == 8080)
41+
42+
// Convenience accessors should agree with the concrete IPv4 value.
43+
#expect(address.host == "127.0.0.1")
44+
#expect(address.port == 8080)
45+
46+
// It must not masquerade as any other address kind.
47+
#expect(address.ipv6 == nil)
48+
#expect(address.unixDomainSocketPath == nil)
49+
}
50+
51+
@available(anyAppleOS 26.0, *)
52+
@Test("An IPv6 NIO address maps to the ipv6 base")
53+
func testIPv6Address() throws {
54+
let nioAddress = try NIOCore.SocketAddress.makeAddressResolvingHost("::1", port: 9090)
55+
let address = try NIOHTTPServer.SocketAddress(nioAddress)
56+
57+
let ipv6 = try #require(address.ipv6)
58+
#expect(ipv6.host == "::1")
59+
#expect(ipv6.port == 9090)
60+
61+
#expect(address.host == "::1")
62+
#expect(address.port == 9090)
63+
64+
#expect(address.ipv4 == nil)
65+
#expect(address.unixDomainSocketPath == nil)
66+
}
67+
68+
@available(anyAppleOS 26.0, *)
69+
@Test("A unix domain socket NIO address maps to the unixDomainSocket base")
70+
func testUnixDomainSocketAddress() throws {
71+
let path = "/tmp/nio-http-server-socket-address-test.sock"
72+
let nioAddress = try NIOCore.SocketAddress(unixDomainSocketPath: path)
73+
let address = try NIOHTTPServer.SocketAddress(nioAddress)
74+
75+
#expect(address.unixDomainSocketPath == path)
76+
77+
// A UDS address exposes neither host nor port.
78+
#expect(address.host == nil)
79+
#expect(address.port == nil)
80+
#expect(address.ipv4 == nil)
81+
#expect(address.ipv6 == nil)
82+
}
83+
}

Tests/NIOHTTPServerTests/Utilities/NIOClient/NIOClient+HTTP1.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import NIOHTTPTypes
1919
import NIOHTTPTypesHTTP1
2020
import NIOPosix
2121

22+
@testable import NIOHTTPServer
23+
2224
@available(anyAppleOS 26.0, *)
2325
extension Channel {
2426
/// Adds HTTP/1.1 client handlers to the pipeline.
@@ -57,12 +59,14 @@ extension ClientBootstrap {
5759
at serverAddress: NIOHTTPServer.SocketAddress
5860
) async throws -> NIOAsyncChannel<HTTPResponsePart, HTTPRequestPart> {
5961
let target: NIOCore.SocketAddress
60-
if let path = serverAddress.unixDomainSocketPath {
62+
63+
switch serverAddress.base {
64+
case .ipv4(let address):
65+
target = try NIOCore.SocketAddress(ipAddress: address.host, port: address.port)
66+
case .ipv6(let address):
67+
target = try NIOCore.SocketAddress(ipAddress: address.host, port: address.port)
68+
case .unixDomainSocket(path: let path):
6169
target = try NIOCore.SocketAddress(unixDomainSocketPath: path)
62-
} else if let host = serverAddress.host, let port = serverAddress.port {
63-
target = try NIOCore.SocketAddress(ipAddress: host, port: port)
64-
} else {
65-
throw TestError.unsupportedAddress
6670
}
6771

6872
return try await self.connect(to: target) { channel in

0 commit comments

Comments
 (0)