Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let package = Package(
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
Expand Down
124 changes: 124 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// swift-tools-version:6.0
Comment thread
pavansai1 marked this conversation as resolved.
Outdated
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-2019 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 PackageDescription

let strictConcurrencyDevelopment = false

let strictConcurrencySettings: [SwiftSetting] = {
var initialSettings: [SwiftSetting] = []

if strictConcurrencyDevelopment {
// -warnings-as-errors here is a workaround so that IDE-based development can
// get tripped up on -require-explicit-sendable.
initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"]))
}

return initialSettings
}()

let package = Package(
name: "async-http-client",
products: [
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.7.1"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
],
targets: [
.target(
name: "CAsyncHTTPClient",
cSettings: [
.define("_GNU_SOURCE")
]
),
.target(
name: "AsyncHTTPClient",
dependencies: [
.target(name: "CAsyncHTTPClient"),
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOTLS", package: "swift-nio"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "Algorithms", package: "swift-algorithms"),
// Observability support
.product(name: "Logging", package: "swift-log"),
.product(name: "Tracing", package: "swift-distributed-tracing"),
],
swiftSettings: strictConcurrencySettings
),
.testTarget(
name: "AsyncHTTPClientTests",
dependencies: [
.target(name: "AsyncHTTPClient"),
.product(name: "NIOTLS", package: "swift-nio"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIOEmbedded", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "NIOTestUtils", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "Algorithms", package: "swift-algorithms"),
// Observability support
.product(name: "Logging", package: "swift-log"),
.product(name: "InMemoryLogging", package: "swift-log"),
.product(name: "Tracing", package: "swift-distributed-tracing"),
.product(name: "InMemoryTracing", package: "swift-distributed-tracing"),
],
resources: [
.copy("Resources/self_signed_cert.pem"),
.copy("Resources/self_signed_key.pem"),
.copy("Resources/example.com.cert.pem"),
.copy("Resources/example.com.private-key.pem"),
],
swiftSettings: strictConcurrencySettings
),
]
)

// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
for target in package.targets {
switch target.type {
case .regular, .test, .executable:
var settings = target.swiftSettings ?? []
// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md
settings.append(.enableUpcomingFeature("MemberImportVisibility"))
target.swiftSettings = settings
case .macro, .plugin, .system, .binary:
() // not applicable
@unknown default:
() // we don't know what to do here, do nothing
}
}
// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- //
2 changes: 1 addition & 1 deletion Package@swift-6.1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let package = Package(
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"),
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,12 @@ extension HTTPConnectionPool.ConnectionFactory {
nioBootstrap
.connectTimeout(deadline - NIODeadline.now())
.enableMPTCP(clientConfiguration.enableMultipath)
switch clientConfiguration.dnsResolver.backing {
case .system:
break
case .randomized:
bootstrap = bootstrap.resolver(NIORandomizedDNSResolver(loop: eventLoop))
Comment thread
pavansai1 marked this conversation as resolved.
}
if let localAddress = self.key.localAddress {
do {
let socketAddress = try SocketAddress(ipAddress: localAddress, port: 0)
Expand Down Expand Up @@ -638,6 +644,12 @@ extension HTTPConnectionPool.ConnectionFactory {
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)
Expand Down
39 changes: 39 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,21 @@ public final class HTTPClient: Sendable {
/// ``HTTPClient`` will still request certificates from the server for `example.com` and validate them as if we would connect to `example.com`.
public var dnsOverride: [String: String] = [:]

/// Controls which DNS resolver is used for hostname resolution.
///
/// By default, the system resolver (`getaddrinfo`) is used, which returns addresses
/// in the order produced by the platform's `getaddrinfo` (typically following
/// RFC 6724 destination-address selection). Set to ``DNSResolver/randomized`` to
/// shuffle addresses for DNS-based load balancing with services that have multiple
/// A/AAAA records (e.g. Kubernetes headless services).
///
/// - Note: This setting has no effect when connections run on an `NIOTSEventLoopGroup`,
/// which is the default on Apple platforms (macOS 10.14+, iOS/tvOS 12+, watchOS 6+).
/// Network.framework performs its own DNS resolution and does not expose a resolver hook.
/// To use the randomized resolver there, pass a `MultiThreadedEventLoopGroup` via
/// ``HTTPClient/EventLoopGroupProvider/shared(_:)``.
public var dnsResolver: DNSResolver = .system

/// Enables following 3xx redirects automatically.
///
/// Following redirects are supported:
Expand Down Expand Up @@ -1418,6 +1433,30 @@ extension HTTPClient.Configuration {
}
}

/// Controls which DNS resolver is used for hostname resolution.
public struct DNSResolver: Sendable, Hashable {
enum Backing: Sendable, Hashable {
case system
case randomized
}

let backing: Backing

private init(backing: Backing) {
self.backing = backing
}

/// Use the system's default DNS resolver (`getaddrinfo`).
/// Addresses are returned in the order produced by the platform's `getaddrinfo`,
/// typically following RFC 6724 destination-address selection.
public static let system: Self = .init(backing: .system)

/// Use a randomized DNS resolver that shuffles the addresses
/// returned by `getaddrinfo`. This enables DNS-based load balancing
/// for services with multiple A/AAAA records (e.g. Kubernetes headless services).
public static let randomized: Self = .init(backing: .randomized)
}

public struct HTTPVersion: Sendable, Hashable {
enum Configuration: String {
case http1Only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2026 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 NIOCore
import NIOHTTP1
import NIOPosix
import NIOSSL
import XCTest

@testable import AsyncHTTPClient

final class RandomizedDNSResolverIntegrationTests: XCTestCase {

func testDefaultDNSResolverIsSystem() {
let config = HTTPClient.Configuration()
XCTAssertEqual(config.dnsResolver, .system)
}

func testRandomizedDNSResolverCanBeSet() {
var config = HTTPClient.Configuration()
config.dnsResolver = .randomized
XCTAssertEqual(config.dnsResolver, .randomized)
}

func testDNSResolverEquality() {
XCTAssertEqual(
HTTPClient.Configuration.DNSResolver.system,
HTTPClient.Configuration.DNSResolver.system
)
XCTAssertEqual(
HTTPClient.Configuration.DNSResolver.randomized,
HTTPClient.Configuration.DNSResolver.randomized
)
XCTAssertNotEqual(
HTTPClient.Configuration.DNSResolver.system,
HTTPClient.Configuration.DNSResolver.randomized
)
}

/// Connect over plain HTTP through the `ClientBootstrap` factory path
/// in `HTTPConnectionPool+Factory.swift`, exercising the `dnsResolver`
/// switch for both `.system` and `.randomized`.
func testResolverConnectsOverPlainHTTP() async throws {
try await self.runConnectTest(ssl: false, resolver: .system)
try await self.runConnectTest(ssl: false, resolver: .randomized)
}

/// Connect over HTTPS through the TLS `ClientBootstrap` factory path
/// in `HTTPConnectionPool+Factory.swift`, exercising the `dnsResolver`
/// switch for both `.system` and `.randomized`.
func testResolverConnectsOverHTTPS() async throws {
try await self.runConnectTest(ssl: true, resolver: .system)
try await self.runConnectTest(ssl: true, resolver: .randomized)
}

private func runConnectTest(
ssl: Bool,
resolver: HTTPClient.Configuration.DNSResolver
) async throws {
let bin = HTTPBin(.http1_1(ssl: ssl, compress: false))
defer { XCTAssertNoThrow(try bin.shutdown()) }

var config = HTTPClient.Configuration()
config.dnsResolver = resolver
if ssl {
config.tlsConfiguration = .clientDefault
config.tlsConfiguration?.certificateVerification = .none
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }

let client = HTTPClient(
eventLoopGroupProvider: .shared(group),
configuration: config
)
defer { XCTAssertNoThrow(try client.syncShutdown()) }

let scheme = ssl ? "https" : "http"
let request = HTTPClientRequest(url: "\(scheme)://localhost:\(bin.port)/get")
let response = try await client.execute(request, deadline: .now() + .seconds(5))
XCTAssertEqual(response.status, .ok)
}
}
Loading