Skip to content

Commit c090008

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 d3362c4 commit c090008

File tree

5 files changed

+274
-37
lines changed

5 files changed

+274
-37
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

+13
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)
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
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

+1-2
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

+16-35
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
}
@@ -1017,21 +1017,19 @@ class HTTPClientTests: XCTestCase {
10171017
XCTFail("Shouldn't succeed")
10181018
continue
10191019
case .failure(let error):
1020-
#if canImport(Network)
10211020
if isTestingNIOTS() {
1022-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1023-
guard let clientError = error as? NWError, case NWError.tls(let status) = clientError else {
1024-
XCTFail("Unexpected error: \(error)")
1025-
continue
1026-
}
1027-
XCTAssertEqual(status, errSSLHandshakeFail)
1021+
#if canImport(Network)
1022+
guard let clientError = error as? NWTLSError else {
1023+
XCTFail("Unexpected error: \(error)")
1024+
continue
1025+
}
1026+
XCTAssertEqual(clientError.status, errSSLHandshakeFail)
1027+
#endif
1028+
} else {
1029+
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1030+
XCTFail("Unexpected error: \(error)")
1031+
continue
10281032
}
1029-
continue
1030-
}
1031-
#endif
1032-
guard let clientError = error as? NIOSSLError, case NIOSSLError.handshakeFailed = clientError else {
1033-
XCTFail("Unexpected error: \(error)")
1034-
continue
10351033
}
10361034
}
10371035
}
@@ -1675,15 +1673,13 @@ class HTTPClientTests: XCTestCase {
16751673
}
16761674

16771675
XCTAssertThrowsError(try httpClient.get(url: "http://localhost:\(port)").wait()) { error in
1678-
#if canImport(Network)
1679-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), isTestingNIOTS() {
1680-
guard let nwError = error as? NWError, case NWError.posix(let posixErrorCode) = nwError, posixErrorCode == .ECONNREFUSED else {
1676+
if isTestingNIOTS() {
1677+
guard let ioError = error as? IOError, ioError.errnoCode == ECONNREFUSED else {
16811678
XCTFail("Unexpected error: \(error)")
16821679
return
16831680
}
16841681
return
16851682
}
1686-
#endif
16871683
guard error is NIOConnectionError else {
16881684
XCTFail("Unexpected error: \(error)")
16891685
return
@@ -1702,19 +1698,4 @@ class HTTPClientTests: XCTestCase {
17021698
}
17031699
XCTAssertNoThrow(try promise.futureResult.wait())
17041700
}
1705-
1706-
1707-
func testCorrectEventLoopGroup() {
1708-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
1709-
defer {
1710-
XCTAssertNoThrow(try httpClient.syncShutdown())
1711-
}
1712-
#if canImport(Network)
1713-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
1714-
XCTAssertTrue(httpClient.eventLoopGroup is NIOTSEventLoopGroup)
1715-
return
1716-
}
1717-
#endif
1718-
XCTAssertTrue(httpClient.eventLoopGroup is MultiThreadedEventLoopGroup)
1719-
}
17201701
}

0 commit comments

Comments
 (0)