Skip to content

Commit cfba7e7

Browse files
committed
Fix leaked TLS handshake promise (#180)
motivation: TLS handshake promise was leaked in some cases of failure (see #179) changes: - Avoid leaking promise - Clearer completion flow for related futures - Add testAvoidLeakingTLSHandshakeCompletionPromise test
1 parent 9cdd1dc commit cfba7e7

File tree

4 files changed

+40
-23
lines changed

4 files changed

+40
-23
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool.swift

+19-18
Original file line numberDiff line numberDiff line change
@@ -383,32 +383,33 @@ final class ConnectionPool {
383383
}
384384

385385
return channel.flatMap { channel -> EventLoopFuture<ConnectionPool.Connection> in
386-
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, handshakePromise: handshakePromise).flatMap {
386+
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, handshakePromise: handshakePromise)
387+
return handshakePromise.futureResult.flatMap {
387388
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
388389
}.map {
389390
let connection = Connection(key: self.key, channel: channel, parentPool: self.parentPool)
390391
connection.isLeased = true
391392
return connection
392393
}
393-
}.flatMap { connection in
394-
handshakePromise.futureResult.map {
395-
self.configureCloseCallback(of: connection)
396-
return connection
397-
}.flatMapError { error in
398-
connection.closePromise.succeed(())
399-
let action = self.parentPool.connectionProvidersLock.withLock {
400-
self.stateLock.withLock {
401-
self.state.failedConnectionAction()
402-
}
403-
}
404-
switch action {
405-
case .makeConnectionAndComplete(let el, let promise):
406-
self.makeConnection(on: el).cascade(to: promise)
407-
case .none:
408-
break
394+
}.map { connection in
395+
self.configureCloseCallback(of: connection)
396+
return connection
397+
}.flatMapError { error in
398+
// This promise may not have been completed if we reach this
399+
// so we fail it to avoid any leak
400+
handshakePromise.fail(error)
401+
let action = self.parentPool.connectionProvidersLock.withLock {
402+
self.stateLock.withLock {
403+
self.state.failedConnectionAction()
409404
}
410-
return self.eventLoop.makeFailedFuture(error)
411405
}
406+
switch action {
407+
case .makeConnectionAndComplete(let el, let promise):
408+
self.makeConnection(on: el).cascade(to: promise)
409+
case .none:
410+
break
411+
}
412+
return self.eventLoop.makeFailedFuture(error)
412413
}
413414
}
414415

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+4-5
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,10 @@ extension ChannelPipeline {
625625
return addHandlers([encoder, decoder, handler])
626626
}
627627

628-
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) -> EventLoopFuture<Void> {
628+
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) {
629629
guard key.scheme == .https else {
630630
handshakePromise.succeed(())
631-
return self.eventLoop.makeSucceededFuture(())
631+
return
632632
}
633633

634634
do {
@@ -638,10 +638,9 @@ extension ChannelPipeline {
638638
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
639639
TLSEventsHandler(completionPromise: handshakePromise),
640640
]
641-
642-
return self.addHandlers(handlers)
641+
self.addHandlers(handlers).cascadeFailure(to: handshakePromise)
643642
} catch {
644-
return self.eventLoop.makeFailedFuture(error)
643+
handshakePromise.fail(error)
645644
}
646645
}
647646
}

Diff for: Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ extension HTTPClientTests {
9595
("testWeRecoverFromServerThatClosesTheConnectionOnUs", testWeRecoverFromServerThatClosesTheConnectionOnUs),
9696
("testPoolClosesIdleConnections", testPoolClosesIdleConnections),
9797
("testRacePoolIdleConnectionsAndGet", testRacePoolIdleConnectionsAndGet),
98+
("testAvoidLeakingTLSHandshakeCompletionPromise", testAvoidLeakingTLSHandshakeCompletionPromise),
9899
]
99100
}
100101
}

Diff for: Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+16
Original file line numberDiff line numberDiff line change
@@ -1656,4 +1656,20 @@ class HTTPClientTests: XCTestCase {
16561656
Thread.sleep(forTimeInterval: 0.01 + .random(in: -0.05...0.05))
16571657
}
16581658
}
1659+
1660+
func testAvoidLeakingTLSHandshakeCompletionPromise() {
1661+
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
1662+
let httpBin = HTTPBin(refusesConnections: true)
1663+
defer {
1664+
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
1665+
XCTAssertNoThrow(try httpBin.shutdown())
1666+
}
1667+
1668+
XCTAssertThrowsError(try httpClient.get(url: "http://localhost/").wait()) { error in
1669+
guard error is NIOConnectionError else {
1670+
XCTFail("Unexpected error: \(error)")
1671+
return
1672+
}
1673+
}
1674+
}
16591675
}

0 commit comments

Comments
 (0)