Skip to content

Commit 639d0d5

Browse files
committed
Translate NWError into easier to use structs
Added NWTLSError, NWDNSError and NWPOSIXError to wrap Network.framework errors. The reason to do this was to make it easier to read Errors thrown from the Network.framework. Added NWError tests
1 parent 9a41fc3 commit 639d0d5

File tree

5 files changed

+274
-37
lines changed

5 files changed

+274
-37
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,13 @@ final class ConnectionPool {
397397
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
398398
return handshakePromise.futureResult.flatMap {
399399
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
400+
}.flatMap {
401+
#if canImport(Network)
402+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
403+
return channel.pipeline.addHandler(NWErrorHandler(), position: .first)
404+
}
405+
#endif
406+
return eventLoop.makeSucceededFuture(())
400407
}.map {
401408
let connection = Connection(key: self.key, channel: channel, parentPool: self.parentPool)
402409
connection.isLeased = true
@@ -406,6 +413,12 @@ final class ConnectionPool {
406413
self.configureCloseCallback(of: connection)
407414
return connection
408415
}.flatMapError { error in
416+
var error = error
417+
#if canImport(Network)
418+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
419+
error = NWErrorHandler.translateError(error)
420+
}
421+
#endif
409422
// This promise may not have been completed if we reach this
410423
// so we fail it to avoid any leak
411424
handshakePromise.fail(error)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2020 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Network)
16+
17+
import Network
18+
import NIO
19+
import NIOHTTP1
20+
import NIOTransportServices
21+
22+
public struct NWDNSError: Error {
23+
24+
/// DNS error type. Error type list is defined in <dns_sd.h>
25+
public let errorType: DNSServiceErrorType
26+
27+
/// actual reason, in human readable form
28+
private let reason: String
29+
30+
/// Initialise a NWDNSError
31+
/// - Parameters:
32+
/// - errorType: DNS error type
33+
/// - reason: String describing reason for error
34+
public init(_ errorType: DNSServiceErrorType, reason: String) {
35+
self.errorType = errorType
36+
self.reason = reason
37+
}
38+
}
39+
40+
extension NWDNSError: CustomStringConvertible {
41+
public var description: String { return reason }
42+
}
43+
44+
public struct NWPOSIXError: Error {
45+
46+
/// POSIX error code (enum)
47+
public let errorCode: POSIXErrorCode
48+
49+
/// actual reason, in human readable form
50+
private let reason: String
51+
52+
/// Initialise a NWPOSIXError
53+
/// - Parameters:
54+
/// - errorType: posix error type
55+
/// - reason: String describing reason for error
56+
public init(_ errorCode: POSIXErrorCode, reason: String) {
57+
self.errorCode = errorCode
58+
self.reason = reason
59+
}
60+
}
61+
62+
extension NWPOSIXError: CustomStringConvertible {
63+
public var description: String { return reason }
64+
}
65+
66+
public struct NWTLSError: Error {
67+
68+
/// TLS error status. List of TLS errors can be found in <Security/SecureTransport.h>
69+
public let status: OSStatus
70+
71+
/// actual reason, in human readable form
72+
private let reason: String
73+
74+
/// initialise a NWTLSError
75+
/// - Parameters:
76+
/// - status: TLS status
77+
/// - reason: String describing reason for error
78+
public init(_ status: OSStatus, reason: String) {
79+
self.status = status
80+
self.reason = reason
81+
}
82+
}
83+
84+
extension NWTLSError: CustomStringConvertible {
85+
public var description: String { return reason }
86+
}
87+
88+
@available (macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
89+
class NWErrorHandler: ChannelInboundHandler {
90+
typealias InboundIn = HTTPClientResponsePart
91+
92+
func errorCaught(context: ChannelHandlerContext, error: Error) {
93+
context.fireErrorCaught(NWErrorHandler.translateError(error))
94+
}
95+
96+
static func translateError(_ error: Error) -> Error {
97+
98+
if let error = error as? NWError {
99+
switch error {
100+
case .dns(let errorType):
101+
return NWDNSError(errorType, reason: error.localizedDescription)
102+
case .tls(let status):
103+
return NWTLSError(status, reason: error.localizedDescription)
104+
case .posix(let errorCode):
105+
return NWPOSIXError(errorCode, reason: error.localizedDescription)
106+
default:
107+
return error
108+
}
109+
}
110+
return error
111+
}
112+
}
113+
114+
115+
#endif
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient 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 AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Network)
16+
17+
@testable import AsyncHTTPClient
18+
import Network
19+
import NIO
20+
import NIOSSL
21+
import NIOTransportServices
22+
import XCTest
23+
24+
class HTTPClientNIOTSTests: XCTestCase {
25+
var clientGroup: EventLoopGroup!
26+
27+
override func setUp() {
28+
XCTAssertNil(self.clientGroup)
29+
self.clientGroup = getDefaultEventLoopGroup(numberOfThreads: 3)
30+
}
31+
32+
override func tearDown() {
33+
XCTAssertNotNil(self.clientGroup)
34+
XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully())
35+
self.clientGroup = nil
36+
}
37+
38+
func testCorrectEventLoopGroup() {
39+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
40+
defer {
41+
XCTAssertNoThrow(try httpClient.syncShutdown())
42+
}
43+
if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
44+
XCTAssertTrue(httpClient.eventLoopGroup is NIOTSEventLoopGroup)
45+
return
46+
}
47+
XCTAssertTrue(httpClient.eventLoopGroup is MultiThreadedEventLoopGroup)
48+
}
49+
50+
func testDNSFailError() {
51+
guard isTestingNIOTS() else { return }
52+
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup))
53+
defer {
54+
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
55+
}
56+
57+
do {
58+
_ = try httpClient.get(url: "http://dnsfail/").wait()
59+
XCTFail("This should have failed")
60+
} catch let error as NWDNSError {
61+
XCTAssertEqual(error.errorType, DNSServiceErrorType(kDNSServiceErr_NoSuchRecord))
62+
} catch {
63+
XCTFail("Error should have been NWDSNError not \(type(of:error))")
64+
}
65+
}
66+
67+
func testTLSFailError() {
68+
guard isTestingNIOTS() else { return }
69+
let httpBin = HTTPBin(ssl: true)
70+
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup))
71+
defer {
72+
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
73+
XCTAssertNoThrow(try httpBin.shutdown())
74+
}
75+
76+
do {
77+
_ = try httpClient.get(url: "https://localhost:\(httpBin.port)/get").wait()
78+
XCTFail("This should have failed")
79+
} catch let error as NWTLSError {
80+
XCTAssertEqual(error.status, errSSLHandshakeFail)
81+
} catch {
82+
XCTFail("Error should have been NWTLSError not \(type(of:error))")
83+
}
84+
}
85+
86+
func testConnectionFailError() {
87+
guard isTestingNIOTS() else { return }
88+
let httpBin = HTTPBin(ssl: true)
89+
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup))
90+
defer {
91+
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
92+
}
93+
let port = httpBin.port
94+
XCTAssertNoThrow(try httpBin.shutdown())
95+
96+
do {
97+
_ = try httpClient.get(url: "https://localhost:\(port)/get").wait()
98+
XCTFail("This should have failed")
99+
} catch let error as NWPOSIXError {
100+
XCTAssertEqual(error.errorCode, .ECONNREFUSED)
101+
} catch {
102+
XCTFail("Error should have been NWPOSIXError not \(type(of:error))")
103+
}
104+
}
105+
106+
func testTLSVersionError() {
107+
guard isTestingNIOTS() else { return }
108+
let httpBin = HTTPBin(ssl: true)
109+
let httpClient = HTTPClient(
110+
eventLoopGroupProvider: .shared(self.clientGroup),
111+
configuration: .init(tlsConfiguration: TLSConfiguration.forClient(minimumTLSVersion: .tlsv11, maximumTLSVersion: .tlsv1, certificateVerification: .none))
112+
)
113+
defer {
114+
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
115+
XCTAssertNoThrow(try httpBin.shutdown())
116+
}
117+
118+
do {
119+
_ = try httpClient.get(url: "https://localhost:\(httpBin.port)/get").wait()
120+
XCTFail("This should have failed")
121+
} catch let error as NWTLSError {
122+
XCTAssertEqual(error.status, errSSLHandshakeFail)
123+
} catch {
124+
XCTFail("Error should have been NWTLSError not \(type(of:error))")
125+
}
126+
}
127+
}
128+
129+
#endif

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ extension HTTPClientTests {
9797
("testPoolClosesIdleConnections", testPoolClosesIdleConnections),
9898
("testRacePoolIdleConnectionsAndGet", testRacePoolIdleConnectionsAndGet),
9999
("testAvoidLeakingTLSHandshakeCompletionPromise", testAvoidLeakingTLSHandshakeCompletionPromise),
100-
("testAsyncShutdown", testAsyncShutdown),
101-
("testCorrectEventLoopGroup", testCorrectEventLoopGroup)
100+
("testAsyncShutdown", testAsyncShutdown)
102101
]
103102
}
104103
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ class HTTPClientTests: XCTestCase {
550550
}
551551

552552
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
553-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
553+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
554554
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
555555
}
556556
}
@@ -567,7 +567,7 @@ class HTTPClientTests: XCTestCase {
567567
}
568568

569569
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
570-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
570+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
571571
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
572572
}
573573
}
@@ -584,7 +584,7 @@ class HTTPClientTests: XCTestCase {
584584
}
585585

586586
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/wrongcontentlength").wait(), "Should fail") { error in
587-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
587+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
588588
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
589589
}
590590
}
@@ -1024,21 +1024,19 @@ class HTTPClientTests: XCTestCase {
10241024
XCTFail("Shouldn't succeed")
10251025
continue
10261026
case .failure(let error):
1027-
#if canImport(Network)
10281027
if isTestingNIOTS() {
1029-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1030-
guard let clientError = error as? NWError, case NWError.tls(let status) = clientError else {
1031-
XCTFail("Unexpected error: \(error)")
1032-
continue
1033-
}
1034-
XCTAssertEqual(status, errSSLHandshakeFail)
1028+
#if canImport(Network)
1029+
guard let clientError = error as? NWTLSError else {
1030+
XCTFail("Unexpected error: \(error)")
1031+
continue
1032+
}
1033+
XCTAssertEqual(clientError.status, errSSLHandshakeFail)
1034+
#endif
1035+
} else {
1036+
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1037+
XCTFail("Unexpected error: \(error)")
1038+
continue
10351039
}
1036-
continue
1037-
}
1038-
#endif
1039-
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1040-
XCTFail("Unexpected error: \(error)")
1041-
continue
10421040
}
10431041
}
10441042
}
@@ -1681,15 +1679,13 @@ class HTTPClientTests: XCTestCase {
16811679
}
16821680

16831681
XCTAssertThrowsError(try httpClient.get(url: "http://localhost:\(port)").wait()) { error in
1684-
#if canImport(Network)
1685-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), isTestingNIOTS() {
1686-
guard let nwError = error as? NWError, case NWError.posix(let posixErrorCode) = nwError, posixErrorCode == .ECONNREFUSED else {
1682+
if isTestingNIOTS() {
1683+
guard let ioError = error as? IOError, ioError.errnoCode == ECONNREFUSED else {
16871684
XCTFail("Unexpected error: \(error)")
16881685
return
16891686
}
16901687
return
16911688
}
1692-
#endif
16931689
guard error is NIOConnectionError else {
16941690
XCTFail("Unexpected error: \(error)")
16951691
return
@@ -1708,19 +1704,4 @@ class HTTPClientTests: XCTestCase {
17081704
}
17091705
XCTAssertNoThrow(try promise.futureResult.wait())
17101706
}
1711-
1712-
1713-
func testCorrectEventLoopGroup() {
1714-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
1715-
defer {
1716-
XCTAssertNoThrow(try httpClient.syncShutdown())
1717-
}
1718-
#if canImport(Network)
1719-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1720-
XCTAssertTrue(httpClient.eventLoopGroup is NIOTSEventLoopGroup)
1721-
return
1722-
}
1723-
#endif
1724-
XCTAssertTrue(httpClient.eventLoopGroup is MultiThreadedEventLoopGroup)
1725-
}
17261707
}

0 commit comments

Comments
 (0)