Skip to content
29 changes: 29 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,32 @@ jobs:
release-builds:
name: Release builds
uses: apple/swift-nio/.github/workflows/release_builds.yml@main

construct-linkage-test-matrix:
name: Construct linkage matrix
runs-on: ubuntu-latest
outputs:
linkage-test-matrix: '${{ steps.generate-matrix.outputs.linkage-test-matrix }}'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- id: generate-matrix
run: echo "linkage-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
env:
MATRIX_LINUX_SETUP_COMMAND: apt-get update -y && apt-get install -yq jq && git config --global --add safe.directory /async-http-client
MATRIX_LINUX_COMMAND: ./scripts/run-linkage-test.sh
MATRIX_LINUX_5_10_ENABLED: false
MATRIX_LINUX_6_0_ENABLED: false
MATRIX_LINUX_6_1_ENABLED: false
MATRIX_LINUX_NIGHTLY_NEXT_ENABLED: false
MATRIX_LINUX_NIGHTLY_MAIN_ENABLED: false

linkage-test:
name: Linkage test
needs: construct-linkage-test-matrix
uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
with:
name: "Linkage test"
matrix_string: '${{ needs.construct-linkage-test-matrix.outputs.linkage-test-matrix }}'
9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ let package = Package(
.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"),
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"),
// Disable all traits to prevent linking Foundation
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: []),
.package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0"),
],
targets: [
Expand All @@ -68,7 +69,11 @@ let package = Package(
.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: "NIOTransportServices",
package: "swift-nio-transport-services",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .macCatalyst, .visionOS])
),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "Algorithms", package: "swift-algorithms"),
.product(name: "Configuration", package: "swift-configuration"),
Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import NIOCore
import NIOHTTP1
import Tracing

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import NIOHTTP1
import NIOSSL
import ServiceContextModule

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClientRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension HTTPClientRequest {
Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
import NIOCore
import NIOHTTP1

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

/// A representation of an HTTP response for the Swift Concurrency HTTPClient API.
///
Expand Down
7 changes: 6 additions & 1 deletion Sources/AsyncHTTPClient/BasicAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
//
//===----------------------------------------------------------------------===//

import Foundation
import NIOHTTP1

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Generates base64 encoded username + password for http basic auth.
///
/// - Parameters:
Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/DeconstructedURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

struct DeconstructedURL {
var scheme: Scheme
Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/FileDownloadDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import NIOCore
import NIOHTTP1
import NIOPosix

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

/// Handles a streaming download to a given file path, allowing headers and progress to be reported.
public final class FileDownloadDelegate: HTTPClientResponseDelegate {
Expand Down
76 changes: 76 additions & 0 deletions Sources/AsyncHTTPClient/FoundationExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
// Extensions which provide better ergonomics when using Foundation types,
// or by using Foundation APIs.

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

extension HTTPClient.Cookie {
/// The cookie's expiration date.
Expand Down Expand Up @@ -73,3 +77,75 @@ extension HTTPClient.Body {
self.bytes(data)
}
}

extension StringProtocol {
func addingPercentEncodingAllowingURLHost() -> String {
guard !self.isEmpty else { return String(self) }

let percent = UInt8(ascii: "%")
let utf8Buffer = self.utf8
let maxLength = utf8Buffer.count * 3
return withUnsafeTemporaryAllocation(of: UInt8.self, capacity: maxLength) { outputBuffer in
var i = 0
for byte in utf8Buffer {
if byte.isURLHostAllowed {
outputBuffer[i] = byte
i += 1
} else {
outputBuffer[i] = percent
outputBuffer[i + 1] = hexToAscii(byte >> 4)
outputBuffer[i + 2] = hexToAscii(byte & 0xF)
i += 3
}
}
return String(decoding: outputBuffer[..<i], as: UTF8.self)
}
}
}

private func asciiToHex(_ ascii: UInt8) -> UInt8? {
switch ascii {
case UInt8(ascii: "0")...UInt8(ascii: "9"): return ascii - UInt8(ascii: "0")
case UInt8(ascii: "A")...UInt8(ascii: "F"): return ascii - UInt8(ascii: "A") + 10
case UInt8(ascii: "a")...UInt8(ascii: "f"): return ascii - UInt8(ascii: "a") + 10
default: return nil
}
}

private func hexToAscii(_ hex: UInt8) -> UInt8 {
switch hex {
case 0x0: return UInt8(ascii: "0")
case 0x1: return UInt8(ascii: "1")
case 0x2: return UInt8(ascii: "2")
case 0x3: return UInt8(ascii: "3")
case 0x4: return UInt8(ascii: "4")
case 0x5: return UInt8(ascii: "5")
case 0x6: return UInt8(ascii: "6")
case 0x7: return UInt8(ascii: "7")
case 0x8: return UInt8(ascii: "8")
case 0x9: return UInt8(ascii: "9")
case 0xA: return UInt8(ascii: "A")
case 0xB: return UInt8(ascii: "B")
case 0xC: return UInt8(ascii: "C")
case 0xD: return UInt8(ascii: "D")
case 0xE: return UInt8(ascii: "E")
case 0xF: return UInt8(ascii: "F")
default: fatalError("Invalid hex digit: \(hex)")
}
}

extension UInt8 {
fileprivate var isURLHostAllowed: Bool {
switch self {
case UInt8(ascii: "0")...UInt8(ascii: "9"),
UInt8(ascii: "A")...UInt8(ascii: "Z"),
UInt8(ascii: "a")...UInt8(ascii: "z"),
UInt8(ascii: "!"), UInt8(ascii: "$"), UInt8(ascii: "&"), UInt8(ascii: "'"),
UInt8(ascii: "("), UInt8(ascii: ")"), UInt8(ascii: "*"), UInt8(ascii: "+"),
UInt8(ascii: ","), UInt8(ascii: "-"), UInt8(ascii: "."), UInt8(ascii: ";"),
UInt8(ascii: "="), UInt8(ascii: "_"), UInt8(ascii: "~"):
return true
default: return false
}
}
}
13 changes: 11 additions & 2 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

import Atomics
import Foundation
import Dispatch
import Logging
import NIOConcurrencyHelpers
import NIOCore
Expand All @@ -22,9 +22,18 @@ import NIOHTTPCompression
import NIOPosix
import NIOSSL
import NIOTLS
import NIOTransportServices
import Tracing

#if canImport(Network)
import NIOTransportServices
#endif

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

extension Logger {
private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value {
"\(request.method) \(request.url)"
Expand Down
19 changes: 16 additions & 3 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
//===----------------------------------------------------------------------===//

import Algorithms
import Foundation
import Logging
import NIOConcurrencyHelpers
import NIOCore
Expand All @@ -22,6 +21,12 @@ import NIOPosix
import NIOSSL
import Tracing

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

extension HTTPClient {
/// A request body.
public struct Body: Sendable {
Expand Down Expand Up @@ -880,7 +885,7 @@ extension URL {
/// - socketPath: The path to the unix domain socket to connect to.
/// - uri: The URI path and query that will be sent to the server.
public init?(httpURLWithSocketPath socketPath: String, uri: String = "/") {
guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil }
guard let host = Self.percentEncodedHost(from: socketPath) else { return nil }
var urlString: String
if uri.hasPrefix("/") {
urlString = "http+unix://\(host)\(uri)"
Expand All @@ -895,7 +900,7 @@ extension URL {
/// - socketPath: The path to the unix domain socket to connect to.
/// - uri: The URI path and query that will be sent to the server.
public init?(httpsURLWithSocketPath socketPath: String, uri: String = "/") {
guard let host = socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return nil }
guard let host = Self.percentEncodedHost(from: socketPath) else { return nil }
var urlString: String
if uri.hasPrefix("/") {
urlString = "https+unix://\(host)\(uri)"
Expand All @@ -904,6 +909,14 @@ extension URL {
}
self.init(string: urlString)
}

private static func percentEncodedHost(from socketPath: String) -> String? {
#if canImport(FoundationEssentials)
socketPath.addingPercentEncodingAllowingURLHost()
#else
socketPath.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
#endif
}
}

protocol HTTPClientTaskDelegate: Sendable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

import NIOCore
import NIOHTTP1
import NIOTransportServices

#if canImport(Network)
import Network
import NIOTransportServices
#endif

extension HTTPClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@

#if canImport(Network)

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
import Dispatch
import Network
import NIOCore
import NIOSSL
Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/RedirectState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import NIOHTTP1

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

typealias RedirectMode = HTTPClient.Configuration.RedirectConfiguration.Mode

Expand Down
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/RequestBag+StateMachine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
import NIOCore
import NIOHTTP1

#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif

extension HTTPClient {
/// The maximum body size allowed, before a redirect response is cancelled. 3KB.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import NIOHTTPCompression
import NIOPosix
import NIOSSL
import NIOTestUtils
import NIOTransportServices
import XCTest

#if canImport(Network)
import Network
import NIOTransportServices
#endif

final class ConnectionPoolSizeConfigValueIsRespectedTests: XCTestCaseHTTPClientTestsBaseClass {
Expand Down
Loading