Skip to content

Commit 86c257f

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 6afdd52 commit 86c257f

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
@@ -96,8 +96,7 @@ extension HTTPClientTests {
9696
("testPoolClosesIdleConnections", testPoolClosesIdleConnections),
9797
("testRacePoolIdleConnectionsAndGet", testRacePoolIdleConnectionsAndGet),
9898
("testAvoidLeakingTLSHandshakeCompletionPromise", testAvoidLeakingTLSHandshakeCompletionPromise),
99-
("testAsyncShutdown", testAsyncShutdown),
100-
("testCorrectEventLoopGroup", testCorrectEventLoopGroup)
99+
("testAsyncShutdown", testAsyncShutdown)
101100
]
102101
}
103102
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ class HTTPClientTests: XCTestCase {
538538
}
539539

540540
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
541-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
541+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
542542
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
543543
}
544544
}
@@ -555,7 +555,7 @@ class HTTPClientTests: XCTestCase {
555555
}
556556

557557
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/noresponse").wait(), "Should fail") { error in
558-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
558+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
559559
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
560560
}
561561
}
@@ -572,7 +572,7 @@ class HTTPClientTests: XCTestCase {
572572
}
573573

574574
XCTAssertThrowsError(try httpClient.get(url: "https://localhost:\(httpBin.port)/wrongcontentlength").wait(), "Should fail") { error in
575-
guard case let error = error as? NIOSSLError, error == .uncleanShutdown else {
575+
guard case let sslError = error as? NIOSSLError, sslError == .uncleanShutdown else {
576576
return XCTFail("Should fail with NIOSSLError.uncleanShutdown")
577577
}
578578
}
@@ -1012,21 +1012,19 @@ class HTTPClientTests: XCTestCase {
10121012
XCTFail("Shouldn't succeed")
10131013
continue
10141014
case .failure(let error):
1015-
#if canImport(Network)
10161015
if isTestingNIOTS() {
1017-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1018-
guard let clientError = error as? NWError, case NWError.tls(let status) = clientError else {
1019-
XCTFail("Unexpected error: \(error)")
1020-
continue
1021-
}
1022-
XCTAssertEqual(status, errSSLHandshakeFail)
1016+
#if canImport(Network)
1017+
guard let clientError = error as? NWTLSError else {
1018+
XCTFail("Unexpected error: \(error)")
1019+
continue
1020+
}
1021+
XCTAssertEqual(clientError.status, errSSLHandshakeFail)
1022+
#endif
1023+
} else {
1024+
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1025+
XCTFail("Unexpected error: \(error)")
1026+
continue
10231027
}
1024-
continue
1025-
}
1026-
#endif
1027-
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1028-
XCTFail("Unexpected error: \(error)")
1029-
continue
10301028
}
10311029
}
10321030
}
@@ -1669,15 +1667,13 @@ class HTTPClientTests: XCTestCase {
16691667
}
16701668

16711669
XCTAssertThrowsError(try httpClient.get(url: "http://localhost:\(port)").wait()) { error in
1672-
#if canImport(Network)
1673-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), isTestingNIOTS() {
1674-
guard let nwError = error as? NWError, case NWError.posix(let posixErrorCode) = nwError, posixErrorCode == .ECONNREFUSED else {
1670+
if isTestingNIOTS() {
1671+
guard let ioError = error as? IOError, ioError.errnoCode == ECONNREFUSED else {
16751672
XCTFail("Unexpected error: \(error)")
16761673
return
16771674
}
16781675
return
16791676
}
1680-
#endif
16811677
guard error is NIOConnectionError else {
16821678
XCTFail("Unexpected error: \(error)")
16831679
return
@@ -1696,19 +1692,4 @@ class HTTPClientTests: XCTestCase {
16961692
}
16971693
XCTAssertNoThrow(try promise.futureResult.wait())
16981694
}
1699-
1700-
1701-
func testCorrectEventLoopGroup() {
1702-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
1703-
defer {
1704-
XCTAssertNoThrow(try httpClient.syncShutdown())
1705-
}
1706-
#if canImport(Network)
1707-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1708-
XCTAssertTrue(httpClient.eventLoopGroup is NIOTSEventLoopGroup)
1709-
return
1710-
}
1711-
#endif
1712-
XCTAssertTrue(httpClient.eventLoopGroup is MultiThreadedEventLoopGroup)
1713-
}
17141695
}

0 commit comments

Comments
 (0)