Skip to content

Commit cd67104

Browse files
Preserve getaddrinfo error code in SocketAddressError (apple#3558)
As observed by @weissi apple#3382 ### Motivation: When `getaddrinfo` fails, SwiftNIO throws `SocketAddressError.unknown` which discards the error code. This makes it impossible for callers to distinguish between different failure reasons (e.g., `EAI_NONAME` vs `EAI_AGAIN`), which is needed for retry logic and diagnostics. ### Modifications: - Add `SocketAddressError.UnknownHost` struct that carries `host`, `port`, `errorCode` (from `getaddrinfo`), and `errorDescription` (from `gai_strerror`). - Update `SocketAddress.makeAddressResolvingHost` and `GetaddrinfoResolver` to throw/fail with the new error type on both POSIX and Windows paths. - Update existing test catch blocks to use the new error type. - Add `testGetaddrinfoErrorCodeIsPreserved` to verify the error code is captured. ### Result: Callers can now inspect the `getaddrinfo` error code when host resolution fails, enabling more specific error handling. Resolves apple#3382 ### Questions for reviewer 1. **Should `case unknown(host:port:)` be deprecated?** — This enum case is no longer thrown after this PR. Callers catching `.unknown` will silently stop matching. Should I add `@available(*, deprecated, renamed: "UnknownHost")` to provide a migration path? 2. **Should the doc comment on `makeAddressResolvingHost` be updated?** — The `- Throws:` line still references `SocketAddressError.unknown` instead of the new `SocketAddressError.UnknownHost`. --------- Co-authored-by: Si Beaumont <beaumont@apple.com>
1 parent e0ee947 commit cd67104

5 files changed

Lines changed: 79 additions & 32 deletions

File tree

Sources/NIOCore/SocketAddresses.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import let WinSDK.INET6_ADDRSTRLEN
2323

2424
import func WinSDK.FreeAddrInfoW
2525
import func WinSDK.GetAddrInfoW
26+
import func WinSDK.gai_strerrorA
2627

2728
import struct WinSDK.ADDRESS_FAMILY
2829
import struct WinSDK.ADDRINFOW
@@ -64,6 +65,7 @@ import CNIOOpenBSD
6465
/// Special `Error` that may be thrown if we fail to create a `SocketAddress`.
6566
public enum SocketAddressError: Error, Equatable, Hashable {
6667
/// The host is unknown (could not be resolved).
68+
@available(*, deprecated, message: "Use SocketAddressError.UnknownHost instead.")
6769
case unknown(host: String, port: Int)
6870
/// The requested `SocketAddress` is not supported.
6971
case unsupported
@@ -82,6 +84,27 @@ extension SocketAddressError {
8284
self.address = address
8385
}
8486
}
87+
88+
public struct UnknownHost: Error, Hashable, CustomStringConvertible {
89+
public var host: String
90+
91+
public var port: Int
92+
93+
public var errorCode: Int
94+
95+
public var errorDescription: String
96+
97+
package init(host: String, port: Int, errorCode: Int, errorDescription: String) {
98+
self.host = host
99+
self.port = port
100+
self.errorCode = errorCode
101+
self.errorDescription = errorDescription
102+
}
103+
104+
public var description: String {
105+
"SocketAddressError.UnknownHost: \(self.errorDescription) (error \(self.errorCode)) for host \(self.host), port \(self.port)"
106+
}
107+
}
85108
}
86109

87110
/// Represent a socket address to which we may want to connect or bind.
@@ -516,7 +539,7 @@ public enum SocketAddress: CustomStringConvertible, Sendable {
516539
/// - host: the hostname which should be resolved.
517540
/// - port: the port itself
518541
/// - Returns: the `SocketAddress` for the host / port pair.
519-
/// - Throws: a `SocketAddressError.unknown` if we could not resolve the `host`, or `SocketAddressError.unsupported` if the address itself is not supported (yet).
542+
/// - Throws: a `SocketAddressError.UnknownHost` if we could not resolve the `host`, or `SocketAddressError.unsupported` if the address itself is not supported (yet).
520543
public static func makeAddressResolvingHost(_ host: String, port: Int) throws -> SocketAddress {
521544
#if os(WASI)
522545
throw SocketAddressError.unsupported
@@ -529,7 +552,12 @@ public enum SocketAddress: CustomStringConvertible, Sendable {
529552

530553
let result = GetAddrInfoW(wszHost, wszPort, nil, &pResult)
531554
guard result == 0 else {
532-
throw SocketAddressError.unknown(host: host, port: port)
555+
throw SocketAddressError.UnknownHost(
556+
host: host,
557+
port: port,
558+
errorCode: Int(result),
559+
errorDescription: String(cString: gai_strerrorA(result))
560+
)
533561
}
534562

535563
defer {
@@ -554,8 +582,14 @@ public enum SocketAddress: CustomStringConvertible, Sendable {
554582
var info: UnsafeMutablePointer<addrinfo>?
555583

556584
// FIXME: this is blocking!
557-
if getaddrinfo(host, String(port), nil, &info) != 0 {
558-
throw SocketAddressError.unknown(host: host, port: port)
585+
let rc = getaddrinfo(host, String(port), nil, &info)
586+
guard rc == 0 else {
587+
throw SocketAddressError.UnknownHost(
588+
host: host,
589+
port: port,
590+
errorCode: Int(rc),
591+
errorDescription: String(cString: gai_strerror(rc))
592+
)
559593
}
560594

561595
defer {

Sources/NIOPosix/GetaddrinfoResolver.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@ internal final class GetaddrinfoResolver: Resolver, Sendable {
147147

148148
let iResult = GetAddrInfoW(wszHost, wszPort, &aiHints, &pResult)
149149
guard iResult == 0 else {
150-
self.fail(SocketAddressError.unknown(host: host, port: port))
150+
self.fail(
151+
SocketAddressError.UnknownHost(
152+
host: host,
153+
port: port,
154+
errorCode: Int(iResult),
155+
errorDescription: String(cString: gai_strerrorA(iResult))
156+
)
157+
)
151158
return
152159
}
153160

@@ -165,8 +172,16 @@ internal final class GetaddrinfoResolver: Resolver, Sendable {
165172
var hint = addrinfo()
166173
hint.ai_socktype = self.aiSocktype.rawValue
167174
hint.ai_protocol = self.aiProtocol.rawValue
168-
guard getaddrinfo(host, String(port), &hint, &info) == 0 else {
169-
self.fail(SocketAddressError.unknown(host: host, port: port))
175+
let rc = getaddrinfo(host, String(port), &hint, &info)
176+
guard rc == 0 else {
177+
self.fail(
178+
SocketAddressError.UnknownHost(
179+
host: host,
180+
port: port,
181+
errorCode: Int(rc),
182+
errorDescription: String(cString: gai_strerror(rc))
183+
)
184+
)
170185
return
171186
}
172187

Tests/NIOPosixTests/DatagramChannelTests.swift

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -761,14 +761,9 @@ class DatagramChannelTests: XCTestCase {
761761
let channel2 = try buildChannel(group: self.group, host: "::1")
762762
try channel2.setOption(.explicitCongestionNotification, value: false).wait()
763763
XCTAssertFalse(try channel2.getOption(.explicitCongestionNotification).wait())
764-
} catch let error as SocketAddressError {
765-
switch error {
766-
case .unknown:
767-
// IPv6 resolution can fail even if supported.
768-
return
769-
case .unsupported, .unixDomainSocketPathTooLong, .failedToParseIPString:
770-
throw error
771-
}
764+
} catch is SocketAddressError.UnknownHost {
765+
// IPv6 resolution can fail even if supported.
766+
return
772767
}
773768
}()
774769
)
@@ -969,14 +964,9 @@ class DatagramChannelTests: XCTestCase {
969964
let channel2 = try buildChannel(group: self.group, host: "::1")
970965
try channel2.setOption(.receivePacketInfo, value: false).wait()
971966
XCTAssertFalse(try channel2.getOption(.receivePacketInfo).wait())
972-
} catch let error as SocketAddressError {
973-
switch error {
974-
case .unknown:
975-
// IPv6 resolution can fail even if supported.
976-
return
977-
case .unsupported, .unixDomainSocketPathTooLong, .failedToParseIPString:
978-
throw error
979-
}
967+
} catch is SocketAddressError.UnknownHost {
968+
// IPv6 resolution can fail even if supported.
969+
return
980970
}
981971
}()
982972
)

Tests/NIOPosixTests/EchoServerClientTest.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -958,15 +958,9 @@ class EchoServerClientTest: XCTestCase {
958958
sem.signal()
959959
return channel.eventLoop.makeSucceededFuture(())
960960
}.bind(host: host, port: 0).wait()
961-
} catch let e as SocketAddressError {
962-
if case .unknown(host, port: 0) = e {
963-
// this can happen if the system isn't configured for both IPv4 and IPv6
964-
continue
965-
} else {
966-
// nope, that's a different socket error
967-
XCTFail("unexpected SocketAddressError: \(e)")
968-
break
969-
}
961+
} catch let e as SocketAddressError.UnknownHost where e.host == host && e.port == 0 {
962+
// this can happen if the system isn't configured for both IPv4 and IPv6
963+
continue
970964
} catch {
971965
// other unknown error
972966
XCTFail("unexpected error: \(error)")

Tests/NIOPosixTests/SocketAddressTest.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,4 +699,18 @@ class SocketAddressTest: XCTestCase {
699699
XCTAssertEqual(SocketAddress(ipv6MaskForPrefix: vector.0), vector.1)
700700
}
701701
}
702+
703+
func testGetaddrinfoErrorCodeIsPreserved() {
704+
// .invalid TLD is guaranteed by RFC 6761 to never resolve.
705+
XCTAssertThrowsError(try SocketAddress.makeAddressResolvingHost("somehost.invalid", port: 80)) { error in
706+
guard let unknownHostError = error as? SocketAddressError.UnknownHost else {
707+
XCTFail("Expected SocketAddressError.UnknownHost, got \(type(of: error)): \(error)")
708+
return
709+
}
710+
XCTAssertEqual(unknownHostError.host, "somehost.invalid")
711+
XCTAssertEqual(unknownHostError.port, 80)
712+
XCTAssertNotEqual(unknownHostError.errorCode, 0, "Error code should be non-zero")
713+
XCTAssertFalse(unknownHostError.errorDescription.isEmpty, "Error description should not be empty")
714+
}
715+
}
702716
}

0 commit comments

Comments
 (0)