Files
async-http-client/Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Factory.swift
T
PAVANSAI KUMAR ALLADI 7744c2a035 Add randomized DNS resolver option for load balancing (#907)
## Summary

- Adds `HTTPClient.Configuration.DNSResolver` with `.system` (default)
and `.randomized`
cases. When `.randomized` is selected, `NIORandomizedDNSResolver` is
installed on the
underlying `ClientBootstrap`, shuffling addresses returned by
`getaddrinfo`.
- Motivation: enables DNS-based load balancing for services that publish
multiple A/AAAA
records (e.g. Kubernetes headless services). The system resolver returns
addresses in a
deterministic order (typically RFC 6724 destination-address selection),
which defeats
  round-robin DNS — clients hammer the first address.

---------

Co-authored-by: Pavan sai kumar alladi <p_alladi@apple.com>
Co-authored-by: George Barnett <gbarnett@apple.com>
2026-06-02 06:56:54 +00:00

744 lines
31 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Logging
import NIOCore
import NIOHTTP1
import NIOHTTPCompression
import NIOPosix
import NIOSOCKS
import NIOSSL
import NIOTLS
#if canImport(Network)
import Network
import NIOTransportServices
#endif
extension HTTPConnectionPool {
struct ConnectionFactory {
let key: ConnectionPool.Key
let clientConfiguration: HTTPClient.Configuration
let tlsConfiguration: TLSConfiguration
let sslContextCache: SSLContextCache
init(
key: ConnectionPool.Key,
tlsConfiguration: TLSConfiguration?,
clientConfiguration: HTTPClient.Configuration,
sslContextCache: SSLContextCache
) {
self.key = key
self.clientConfiguration = clientConfiguration
self.sslContextCache = sslContextCache
self.tlsConfiguration =
tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .makeClientConfiguration()
}
}
}
protocol HTTPConnectionRequester: Sendable {
func http1ConnectionCreated(_: HTTP1Connection.SendableView)
func http2ConnectionCreated(_: HTTP2Connection.SendableView, maximumStreams: Int)
func failedToCreateHTTPConnection(_: HTTPConnectionPool.Connection.ID, error: Error)
func waitingForConnectivity(_: HTTPConnectionPool.Connection.ID, error: Error)
}
extension HTTPConnectionPool.ConnectionFactory {
func makeConnection<Requester: HTTPConnectionRequester>(
for requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
http1ConnectionDelegate: HTTP1ConnectionDelegate,
http2ConnectionDelegate: HTTP2ConnectionDelegate,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger
) {
var logger = logger
logger[metadataKey: "ahc-connection-id"] = "\(connectionID)"
let promise = eventLoop.makePromise(of: NegotiatedProtocol.self)
promise.futureResult.whenComplete { [logger] result in
switch result {
case .success(.http1_1(let channel)):
do {
let connection = try HTTP1Connection.start(
channel: channel,
connectionID: connectionID,
delegate: http1ConnectionDelegate,
decompression: self.clientConfiguration.decompression,
logger: logger
)
if let connectionDebugInitializer = self.clientConfiguration.http1_1ConnectionDebugInitializer {
connectionDebugInitializer(channel).hop(
to: eventLoop
).assumeIsolated().whenComplete { debugInitializerResult in
switch debugInitializerResult {
case .success:
requester.http1ConnectionCreated(connection.sendableView)
case .failure(let error):
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
}
} else {
requester.http1ConnectionCreated(connection.sendableView)
}
} catch {
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
case .success(.http2(let channel)):
HTTP2Connection.start(
channel: channel,
connectionID: connectionID,
delegate: http2ConnectionDelegate,
decompression: self.clientConfiguration.decompression,
maximumConnectionUses: self.clientConfiguration.maximumUsesPerConnection,
logger: logger,
streamChannelDebugInitializer:
self.clientConfiguration.http2StreamChannelDebugInitializer
).whenComplete { result in
switch result {
case .success((let connection, let maximumStreams)):
if let connectionDebugInitializer = self.clientConfiguration.http2ConnectionDebugInitializer {
connectionDebugInitializer(channel).hop(to: eventLoop).assumeIsolated().whenComplete {
debugInitializerResult in
switch debugInitializerResult {
case .success:
requester.http2ConnectionCreated(
connection.sendableView,
maximumStreams: maximumStreams
)
case .failure(let error):
requester.failedToCreateHTTPConnection(
connectionID,
error: error
)
}
}
} else {
requester.http2ConnectionCreated(
connection.sendableView,
maximumStreams: maximumStreams
)
}
case .failure(let error):
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
}
case .failure(var error):
// let's map `ChannelError.connectTimeout` into a `HTTPClientError.connectTimeout`
switch error {
case ChannelError.connectTimeout:
error = HTTPClientError.connectTimeout
default:
()
}
requester.failedToCreateHTTPConnection(connectionID, error: error)
}
}
self.makeChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger,
promise: promise
)
}
enum NegotiatedProtocol {
case http1_1(Channel)
case http2(Channel)
}
func makeChannel<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger,
promise: EventLoopPromise<NegotiatedProtocol>
) {
if self.key.scheme.isProxyable, let proxy = self.clientConfiguration.proxy {
switch proxy.type {
case .socks:
self.makeSOCKSProxyChannel(
proxy,
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger,
promise: promise
)
case .http:
self.makeHTTPProxyChannel(
proxy,
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger,
promise: promise
)
}
} else {
self.makeNonProxiedChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger,
promise: promise
)
}
}
private func makeNonProxiedChannel<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger,
promise: EventLoopPromise<NegotiatedProtocol>
) {
switch self.key.scheme {
case .http, .httpUnix, .unix:
self.makePlainChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
promise: promise
)
case .https, .httpsUnix:
self.makeTLSChannel(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger,
promise: promise
)
}
}
private func makePlainChannel<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
promise: EventLoopPromise<NegotiatedProtocol>
) {
precondition(!self.key.scheme.usesTLS, "Unexpected scheme")
do {
let bootstrap = try self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
)
bootstrap.connect(target: self.key.connectionTarget).map {
.http1_1($0)
}.cascade(to: promise)
} catch {
promise.fail(error)
}
}
private func makeHTTPProxyChannel<Requester: HTTPConnectionRequester>(
_ proxy: HTTPClient.Configuration.Proxy,
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger,
promise: EventLoopPromise<NegotiatedProtocol>
) {
// A proxy connection starts with a plain text connection to the proxy server. After
// the connection has been established with the proxy server, the connection might be
// upgraded to TLS before we send our first request.
let bootstrap: NIOClientTCPBootstrapProtocol
do {
bootstrap = try self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
)
} catch {
promise.fail(error)
return
}
bootstrap.connect(host: proxy.host, port: proxy.port).whenComplete { result in
switch result {
case .success(let channel):
let encoder = HTTPRequestEncoder()
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes))
let proxyHandler = HTTP1ProxyConnectHandler(
target: self.key.connectionTarget,
proxyAuthorization: proxy.authorization,
deadline: deadline
)
do {
try channel.pipeline.syncOperations.addHandler(encoder)
try channel.pipeline.syncOperations.addHandler(decoder)
try channel.pipeline.syncOperations.addHandler(proxyHandler)
} catch {
return promise.fail(error)
}
// The proxyEstablishedFuture is set as soon as the HTTP1ProxyConnectHandler is in a
// pipeline. It is created in HTTP1ProxyConnectHandler's handlerAdded method.
return proxyHandler.proxyEstablishedFuture!.assumeIsolated().flatMap {
channel.pipeline.syncOperations.removeHandler(proxyHandler).assumeIsolated().flatMap {
channel.pipeline.syncOperations.removeHandler(decoder).assumeIsolated().flatMap {
channel.pipeline.syncOperations.removeHandler(encoder)
}.nonisolated()
}.nonisolated()
}.flatMap {
self.setupTLSInProxyConnectionIfNeeded(channel, deadline: deadline, logger: logger)
}.nonisolated().cascade(to: promise)
case .failure(let error):
promise.fail(error)
}
}
}
private func makeSOCKSProxyChannel<Requester: HTTPConnectionRequester>(
_ proxy: HTTPClient.Configuration.Proxy,
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger,
promise: EventLoopPromise<NegotiatedProtocol>
) {
// A proxy connection starts with a plain text connection to the proxy server. After
// the connection has been established with the proxy server, the connection might be
// upgraded to TLS before we send our first request.
let bootstrap: NIOClientTCPBootstrapProtocol
do {
bootstrap = try self.makePlainBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop
)
} catch {
promise.fail(error)
return
}
bootstrap.connect(host: proxy.host, port: proxy.port).whenComplete { result in
switch result {
case .success(let channel):
let socksConnectHandler = SOCKSClientHandler(targetAddress: SOCKSAddress(self.key.connectionTarget))
let socksEventHandler = SOCKSEventsHandler(deadline: deadline)
do {
try channel.pipeline.syncOperations.addHandler(socksConnectHandler)
try channel.pipeline.syncOperations.addHandler(socksEventHandler)
} catch {
return promise.fail(error)
}
// The socksEstablishedFuture is set as soon as the SOCKSEventsHandler is in a
// pipeline. It is created in SOCKSEventsHandler's handlerAdded method.
socksEventHandler.socksEstablishedFuture!.assumeIsolated().flatMap {
channel.pipeline.syncOperations.removeHandler(socksEventHandler).assumeIsolated().flatMap {
channel.pipeline.syncOperations.removeHandler(socksConnectHandler)
}.nonisolated()
}.flatMap {
self.setupTLSInProxyConnectionIfNeeded(channel, deadline: deadline, logger: logger)
}.nonisolated().cascade(to: promise)
case .failure(let error):
promise.fail(error)
}
}
}
private func setupTLSInProxyConnectionIfNeeded(
_ channel: Channel,
deadline: NIODeadline,
logger: Logger
) -> EventLoopFuture<NegotiatedProtocol> {
switch self.key.scheme {
case .unix, .httpUnix, .httpsUnix:
preconditionFailure("Unexpected scheme. Not supported for proxy!")
case .http:
return channel.eventLoop.makeSucceededFuture(.http1_1(channel))
case .https:
var tlsConfig = self.tlsConfiguration
switch self.clientConfiguration.httpVersion.configuration {
case .automatic:
// since we can support h2, we need to advertise this in alpn
// "ProtocolNameList" contains the list of protocols advertised by the
// client, in descending order of preference.
// https://datatracker.ietf.org/doc/html/rfc7301#section-3.1
tlsConfig.applicationProtocols = ["h2", "http/1.1"]
case .http1Only:
tlsConfig.applicationProtocols = ["http/1.1"]
}
let sslServerHostname = self.key.serverNameIndicator
let sslContextFuture = self.sslContextCache.sslContext(
tlsConfiguration: tlsConfig,
eventLoop: channel.eventLoop,
logger: logger
)
return sslContextFuture.flatMap { sslContext -> EventLoopFuture<String?> in
do {
let sslHandler = try NIOSSLClientHandler(
context: sslContext,
serverHostname: sslServerHostname
)
try channel.pipeline.syncOperations.addHandler(sslHandler)
let tlsEventHandler = TLSEventsHandler(deadline: deadline)
try channel.pipeline.syncOperations.addHandler(tlsEventHandler)
// The tlsEstablishedFuture is set as soon as the TLSEventsHandler is in a
// pipeline. It is created in TLSEventsHandler's handlerAdded method.
return tlsEventHandler.tlsEstablishedFuture!
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}.flatMap { negotiated -> EventLoopFuture<NegotiatedProtocol> in
do {
let sync = channel.pipeline.syncOperations
let context = try sync.context(handlerType: TLSEventsHandler.self)
return sync.removeHandler(context: context).flatMapThrowing {
try Self.matchALPNToHTTPVersion(negotiated, channel: channel)
}
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
}
}
private func makePlainBootstrap<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop
) throws -> NIOClientTCPBootstrapProtocol {
if let localAddress = self.key.localAddress, !localAddress.isIPAddress {
throw HTTPClientError.invalidLocalAddress
}
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *),
let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop)
{
var bootstrap =
tsBootstrap
.channelOption(
NIOTSChannelOptions.waitForActivity,
value: self.clientConfiguration.networkFrameworkWaitForConnectivity
)
.channelOption(
NIOTSChannelOptions.multipathServiceType,
value: self.clientConfiguration.enableMultipath ? .handover : .disabled
)
.connectTimeout(deadline - NIODeadline.now())
.channelInitializer { channel in
do {
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
try channel.pipeline.syncOperations.addHandler(
NWWaitingHandler(requester: requester, connectionID: connectionID)
)
return channel.eventLoop.makeSucceededVoidFuture()
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
if let localAddress = self.key.localAddress {
bootstrap = bootstrap.configureNWParameters { params in
params.requiredLocalEndpoint = NWEndpoint.hostPort(
host: NWEndpoint.Host(localAddress),
port: .any
)
}
}
return bootstrap
}
#endif
if let nioBootstrap = ClientBootstrap(validatingGroup: eventLoop) {
var bootstrap =
nioBootstrap
.connectTimeout(deadline - NIODeadline.now())
.enableMPTCP(clientConfiguration.enableMultipath)
switch clientConfiguration.dnsResolver.backing {
case .system:
break
case .randomized:
bootstrap = bootstrap.resolver(NIORandomizedDNSResolver(loop: eventLoop))
}
if let localAddress = self.key.localAddress {
do {
let socketAddress = try SocketAddress(ipAddress: localAddress, port: 0)
bootstrap = bootstrap.bind(to: socketAddress)
} catch {
throw HTTPClientError.invalidLocalAddress
}
}
return bootstrap
}
preconditionFailure("No matching bootstrap found")
}
private func makeTLSChannel<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger,
promise: EventLoopPromise<NegotiatedProtocol>
) {
precondition(self.key.scheme.usesTLS, "Unexpected scheme")
let bootstrapFuture = self.makeTLSBootstrap(
requester: requester,
connectionID: connectionID,
deadline: deadline,
eventLoop: eventLoop,
logger: logger
)
bootstrapFuture.whenComplete { result in
switch result {
case .success(let bootstrap):
bootstrap.connect(target: self.key.connectionTarget).flatMap {
channel -> EventLoopFuture<(Channel, String?)> in
do {
// if the channel is closed before flatMap is executed, all ChannelHandler are removed
// and TLSEventsHandler is therefore not present either
let tlsEventHandler = try channel.pipeline.syncOperations.handler(type: TLSEventsHandler.self)
// The tlsEstablishedFuture is set as soon as the TLSEventsHandler is in a
// pipeline. It is created in TLSEventsHandler's handlerAdded method.
return tlsEventHandler.tlsEstablishedFuture!.assumeIsolated().flatMap { negotiated in
channel.pipeline.syncOperations.removeHandler(tlsEventHandler).map { (channel, negotiated) }
}.nonisolated()
} catch {
assert(
channel.isActive == false,
"if the channel is still active then TLSEventsHandler must be present but got error \(error)"
)
return channel.eventLoop.makeFailedFuture(HTTPClientError.remoteConnectionClosed)
}
}.flatMapThrowing { channel, alpn in
try Self.matchALPNToHTTPVersion(alpn, channel: channel)
}.flatMapErrorThrowing { error in
// If NIOTransportSecurity is used, we want to map NWErrors into NWPOsixErrors or NWTLSError.
#if canImport(Network)
throw HTTPClient.NWErrorHandler.translateError(error)
#else
throw error
#endif
}.cascade(to: promise)
case .failure(let error):
promise.fail(error)
}
}
}
private func makeTLSBootstrap<Requester: HTTPConnectionRequester>(
requester: Requester,
connectionID: HTTPConnectionPool.Connection.ID,
deadline: NIODeadline,
eventLoop: EventLoop,
logger: Logger
) -> EventLoopFuture<NIOClientTCPBootstrapProtocol> {
if let localAddress = self.key.localAddress, !localAddress.isIPAddress {
return eventLoop.makeFailedFuture(HTTPClientError.invalidLocalAddress)
}
var tlsConfig = self.tlsConfiguration
switch self.clientConfiguration.httpVersion.configuration {
case .automatic:
// since we can support h2, we need to advertise this in alpn
// "ProtocolNameList" contains the list of protocols advertised by the
// client, in descending order of preference.
// https://datatracker.ietf.org/doc/html/rfc7301#section-3.1
tlsConfig.applicationProtocols = ["h2", "http/1.1"]
case .http1Only:
tlsConfig.applicationProtocols = ["http/1.1"]
}
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), eventLoop is QoSEventLoop {
// create NIOClientTCPBootstrap with NIOTS TLS provider
let localAddr = self.key.localAddress
let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(
on: eventLoop,
serverNameIndicatorOverride: key.serverNameIndicatorOverride
).map {
options -> NIOClientTCPBootstrapProtocol in
var bootstrap = NIOTSConnectionBootstrap(group: eventLoop) // validated above
.channelOption(
NIOTSChannelOptions.waitForActivity,
value: self.clientConfiguration.networkFrameworkWaitForConnectivity
)
.channelOption(
NIOTSChannelOptions.multipathServiceType,
value: self.clientConfiguration.enableMultipath ? .handover : .disabled
)
.connectTimeout(deadline - NIODeadline.now())
.tlsOptions(options)
.channelInitializer { channel in
do {
try channel.pipeline.syncOperations.addHandler(HTTPClient.NWErrorHandler())
try channel.pipeline.syncOperations.addHandler(
NWWaitingHandler(requester: requester, connectionID: connectionID)
)
// we don't need to set a TLS deadline for NIOTS connections, since the
// TLS handshake is part of the TS connection bootstrap. If the TLS
// handshake times out the complete connection creation will be failed.
try channel.pipeline.syncOperations.addHandler(TLSEventsHandler(deadline: nil))
return channel.eventLoop.makeSucceededVoidFuture()
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
if let localAddress = localAddr {
bootstrap = bootstrap.configureNWParameters { params in
params.requiredLocalEndpoint = NWEndpoint.hostPort(
host: NWEndpoint.Host(localAddress),
port: .any
)
}
}
return bootstrap as NIOClientTCPBootstrapProtocol
}
return bootstrapFuture
}
#endif
let sslContextFuture = sslContextCache.sslContext(
tlsConfiguration: tlsConfig,
eventLoop: eventLoop,
logger: logger
)
return eventLoop.submit { [key] () throws -> NIOClientTCPBootstrapProtocol in
var bootstrap = ClientBootstrap(group: eventLoop)
.connectTimeout(deadline - NIODeadline.now())
.enableMPTCP(clientConfiguration.enableMultipath)
switch clientConfiguration.dnsResolver.backing {
case .system:
break
case .randomized:
bootstrap = bootstrap.resolver(NIORandomizedDNSResolver(loop: eventLoop))
}
if let localAddress = key.localAddress {
do {
let socketAddress = try SocketAddress(ipAddress: localAddress, port: 0)
bootstrap = bootstrap.bind(to: socketAddress)
} catch {
throw HTTPClientError.invalidLocalAddress
}
}
return
bootstrap
.channelInitializer { channel in
sslContextFuture.flatMap { sslContext -> EventLoopFuture<Void> in
do {
let sync = channel.pipeline.syncOperations
let sslHandler = try NIOSSLClientHandler(
context: sslContext,
serverHostname: self.key.serverNameIndicator
)
let tlsEventHandler = TLSEventsHandler(deadline: deadline)
try sync.addHandler(sslHandler)
try sync.addHandler(tlsEventHandler)
return channel.eventLoop.makeSucceededVoidFuture()
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}
}
}
}
private static func matchALPNToHTTPVersion(_ negotiated: String?, channel: Channel) throws -> NegotiatedProtocol {
switch negotiated {
case .none, .some("http/1.1"):
return .http1_1(channel)
case .some("h2"):
return .http2(channel)
case .some(let unsupported):
throw HTTPClientError.serverOfferedUnsupportedApplicationProtocol(unsupported)
}
}
}
extension Scheme {
var isProxyable: Bool {
switch self {
case .http, .https:
return true
case .unix, .httpUnix, .httpsUnix:
return false
}
}
}
extension ConnectionPool.Key {
var serverNameIndicator: String? {
serverNameIndicatorOverride ?? connectionTarget.sslServerHostname
}
}
extension ConnectionTarget {
fileprivate var sslServerHostname: String? {
switch self {
case .domain(let domain, _): return domain
case .ipAddress, .unixSocket: return nil
}
}
}
extension SOCKSAddress {
fileprivate init(_ host: ConnectionTarget) {
switch host {
case .ipAddress(_, let address): self = .address(address)
case .domain(let domain, let port): self = .domain(domain, port: port)
case .unixSocket: fatalError("Unix Domain Sockets are not supported by SOCKSAddress")
}
}
}
extension NIOClientTCPBootstrapProtocol {
func connect(target: ConnectionTarget) -> EventLoopFuture<Channel> {
switch target {
case .ipAddress(_, let socketAddress):
return self.connect(to: socketAddress)
case .domain(let domain, let port):
return self.connect(host: domain, port: port)
case .unixSocket(let path):
return self.connect(unixDomainSocketPath: path)
}
}
}