From 29e27d3e7d358c6adff734f33d99ceb8daf56312 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 3 Mar 2026 17:02:16 +0100 Subject: [PATCH 01/11] Land AHC conformance --- Package.swift | 23 +++ Sources/AHCConformance/AHC/AHC+HTTP.swift | 227 ++++++++++++++++++++++ Tests/AHCConformanceTests/Suite.swift | 49 +++++ 3 files changed, 299 insertions(+) create mode 100644 Sources/AHCConformance/AHC/AHC+HTTP.swift create mode 100644 Tests/AHCConformanceTests/Suite.swift diff --git a/Package.swift b/Package.swift index fd2d7ff..dc787ec 100644 --- a/Package.swift +++ b/Package.swift @@ -44,6 +44,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.30.0"), .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"), + + .package(url: "https://github.com/swift-server/async-http-client.git", branch: "ff-spi-for-httpapis"), ], targets: [ // MARK: Libraries @@ -85,6 +87,20 @@ let package = Package( name: "Middleware", swiftSettings: extraSettings ), + .target( + name: "AHCConformance", + dependencies: [ + "HTTPAPIs", + "AsyncStreaming", + "NetworkTypes", + .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "HTTPTypesFoundation", package: "swift-http-types"), + + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOHTTP1", package: "swift-nio"), + ], + swiftSettings: extraSettings + ), // MARK: Conformance Testing @@ -145,6 +161,13 @@ let package = Package( ], swiftSettings: extraSettings ), + .testTarget( + name: "AHCConformanceTests", + dependencies: [ + "AHCConformance", + "HTTPClientConformance", + ] + ), .testTarget( name: "HTTPClientTests", dependencies: [ diff --git a/Sources/AHCConformance/AHC/AHC+HTTP.swift b/Sources/AHCConformance/AHC/AHC+HTTP.swift new file mode 100644 index 0000000..2e3f338 --- /dev/null +++ b/Sources/AHCConformance/AHC/AHC+HTTP.swift @@ -0,0 +1,227 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +@_spi(ExperimentalHTTPAPIsSupport) public import AsyncHTTPClient +import BasicContainers +import Foundation +public import HTTPAPIs +import HTTPTypes +import NIOCore +import NIOHTTP1 +import Synchronization + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, *) +extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { + public typealias RequestWriter = RequestBodyWriter + public typealias ResponseConcludingReader = ResponseReader + + public struct RequestOptions: HTTPClientCapability.RequestOptions { + public init() {} + } + + public struct RequestBodyWriter: AsyncWriter, ~Copyable { + public typealias WriteElement = UInt8 + public typealias WriteFailure = any Error + + let requestWriter: HTTPClientRequest.Body.RequestWriter + var byteBuffer: ByteBuffer + var rigidArray: RigidArray + + init(_ requestWriter: HTTPClientRequest.Body.RequestWriter) { + self.requestWriter = requestWriter + self.byteBuffer = ByteBuffer() + self.byteBuffer.reserveCapacity(2 ^ 16) + self.rigidArray = RigidArray(capacity: 2 ^ 16) // ~ 65k bytes + } + + public mutating func write( + _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result + ) async throws(AsyncStreaming.EitherError) -> Result where Failure: Error { + let result: Result + do { + // TODO: rigidArray needs a clear all + self.rigidArray.removeAll() + self.rigidArray.reserveCapacity(1024) + result = try await self.rigidArray.append(count: 1024) { (span) async throws(Failure) -> Result in + try await body(&span) + } + + if self.rigidArray.isEmpty { + return result + } + } catch { + throw .second(error) + } + + do { + self.byteBuffer.clear() + + // we need to use an uninitilized helper rigidarray here to make the compiler happy + // with regards overlapping memory access. + var localArray = RigidArray(capacity: 0) + swap(&localArray, &self.rigidArray) + unsafe localArray.span.withUnsafeBufferPointer { bufferPtr in + unsafe self.byteBuffer.withUnsafeMutableWritableBytes { byteBufferPtr in + unsafe byteBufferPtr.copyBytes(from: bufferPtr) + } + self.byteBuffer.moveWriterIndex(forwardBy: bufferPtr.count) + } + + swap(&localArray, &self.rigidArray) + try await self.requestWriter.writeRequestBodyPart(self.byteBuffer) + } catch { + throw .first(error) + } + + return result + } + } + + public struct ResponseReader: ConcludingAsyncReader { + public typealias Underlying = ResponseBodyReader + + let underlying: HTTPClientResponse.Body + + public typealias FinalElement = HTTPFields? + + init(underlying: HTTPClientResponse.Body) { + self.underlying = underlying + } + + public consuming func consumeAndConclude( + body: + nonisolated(nonsending) (consuming sending ResponseBodyReader) async throws(Failure) -> + Return + ) async throws(Failure) -> (Return, HTTPFields?) where Failure: Error { + let iterator = self.underlying.makeAsyncIterator() + let reader = ResponseBodyReader(underlying: iterator) + let returnValue = try await body(reader) + + let t = self.underlying.trailers?.compactMap { + if let name = HTTPField.Name($0.name) { + HTTPField(name: name, value: $0.value) + } else { + nil + } + } + return (returnValue, t.flatMap({ HTTPFields($0) })) + } + } + + public struct ResponseBodyReader: AsyncReader, ~Copyable { + public typealias ReadElement = UInt8 + public typealias ReadFailure = any Error + + var underlying: HTTPClientResponse.Body.AsyncIterator + + public mutating func read( + maximumCount: Int?, + body: nonisolated(nonsending) (consuming Span) async throws(Failure) -> Return + ) async throws(AsyncStreaming.EitherError) -> Return where Failure: Error { + + do { + let buffer = try await self.underlying.next(isolation: #isolation) + guard let buffer else { + let array = InlineArray<0, UInt8> { _ in } + return try await body(array.span) + } + var array = RigidArray() + array.reserveCapacity(buffer.readableBytes) + unsafe buffer.withUnsafeReadableBytes { rawBufferPtr in + let usbptr = unsafe rawBufferPtr.assumingMemoryBound(to: UInt8.self) + unsafe array.append(copying: usbptr) + } + return try await body(array.span) + } catch let error as Failure { + throw .second(error) + } catch { + throw .first(error) + } + } + } + + public func perform( + request: HTTPRequest, + body: consuming HTTPClientRequestBody?, + options: RequestOptions, + responseHandler: nonisolated(nonsending) (HTTPResponse, consuming ResponseReader) async throws -> Return + ) async throws -> Return { + guard let url = request.url else { + fatalError() + } + + var result: Result? + await withTaskGroup(of: Void.self) { taskGroup in + + var ahcRequest = HTTPClientRequest(url: url.absoluteString) + ahcRequest.method = .init(rawValue: request.method.rawValue) + if !request.headerFields.isEmpty { + let sequence = request.headerFields.lazy.map({ ($0.name.rawName, $0.value) }) + ahcRequest.headers.add(contentsOf: sequence) + } + + if let body, body.knownLength != 0 { + let (asyncStream, startUploadContinuation) = AsyncStream.makeStream(of: HTTPClientRequest.Body.RequestWriter.self) + + taskGroup.addTask { + // TODO: We might want to allow multiple body restarts here. + + for await ahcWriter in asyncStream { + do { + let writer = RequestWriter(ahcWriter) + let maybeTrailers = try await body.produce(into: writer) + let trailers: HTTPHeaders? = + if let trailers = maybeTrailers { + HTTPHeaders(.init(trailers.lazy.map({ ($0.name.rawName, $0.value) }))) + } else { + nil + } + ahcWriter.requestBodyStreamFinished(trailers: trailers) + break // the loop + } catch let error { + // if we fail because the user throws in upload, we have to cancel the + // upload and fail the request I guess. + ahcWriter.fail(error) + } + } + } + + ahcRequest.body = .init(length: body.knownLength, startUpload: startUploadContinuation) + } + + do { + let ahcResponse = try await self.execute(ahcRequest, timeout: .seconds(30)) + + var responseFields = HTTPFields() + for (name, value) in ahcResponse.headers { + if let name = HTTPField.Name(name) { + // Add a new header field + responseFields.append(.init(name: name, value: value)) + } + } + + let response = HTTPResponse( + status: .init(code: Int(ahcResponse.status.code)), + headerFields: responseFields + ) + + result = .success(try await responseHandler(response, .init(underlying: ahcResponse.body))) + } catch { + result = .failure(error) + } + } + + return try result!.get() + } +} diff --git a/Tests/AHCConformanceTests/Suite.swift b/Tests/AHCConformanceTests/Suite.swift new file mode 100644 index 0000000..e5400ce --- /dev/null +++ b/Tests/AHCConformanceTests/Suite.swift @@ -0,0 +1,49 @@ +//===----------------------------------------------------------------------===// +// +// 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 AHCConformance +import AsyncHTTPClient +import HTTPAPIs +import HTTPClient +import HTTPClientConformance +import Testing + +@Suite struct AsyncHTTPClientTests { + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) + @Test func conformance() async throws { + var config = HTTPClient.Configuration() + config.connectionPool.concurrentHTTP1ConnectionsPerHostSoftLimit = 1 + config.httpVersion = .automatic + config.decompression = .enabled(limit: .none) + let httpClient = HTTPClient(eventLoopGroup: .singletonMultiThreadedEventLoopGroup, configuration: config) + defer { Task { try await httpClient.shutdown() } } + + try await runBasicConformanceTests { + httpClient + } + } +} + +@available(macOS 26.2, *) +extension AsyncHTTPClient.HTTPClient.RequestOptions: HTTPClientCapability.RedirectionHandler { + @available(macOS 26.2, *) + public var redirectionHandler: (any HTTPClientRedirectionHandler)? { + get { + nil + } + set { + + } + } +} From 584d1b9875c8531721df5db1fa29713268b07bb7 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 3 Mar 2026 17:54:29 +0100 Subject: [PATCH 02/11] fix some tests --- Sources/AHCConformance/AHC/AHC+HTTP.swift | 6 ++++-- Sources/HTTPClientConformance/HTTPClientConformance.swift | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/AHCConformance/AHC/AHC+HTTP.swift b/Sources/AHCConformance/AHC/AHC+HTTP.swift index 2e3f338..5aa42ca 100644 --- a/Sources/AHCConformance/AHC/AHC+HTTP.swift +++ b/Sources/AHCConformance/AHC/AHC+HTTP.swift @@ -72,6 +72,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { var localArray = RigidArray(capacity: 0) swap(&localArray, &self.rigidArray) unsafe localArray.span.withUnsafeBufferPointer { bufferPtr in + self.byteBuffer.reserveCapacity(bufferPtr.count) unsafe self.byteBuffer.withUnsafeMutableWritableBytes { byteBufferPtr in unsafe byteBufferPtr.copyBytes(from: bufferPtr) } @@ -137,10 +138,11 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { return try await body(array.span) } var array = RigidArray() - array.reserveCapacity(buffer.readableBytes) + let capcity = maximumCount != nil ? min(maximumCount!, buffer.readableBytes) : buffer.readableBytes + array.reserveCapacity(capcity) unsafe buffer.withUnsafeReadableBytes { rawBufferPtr in let usbptr = unsafe rawBufferPtr.assumingMemoryBound(to: UInt8.self) - unsafe array.append(copying: usbptr) + unsafe array.append(copying: usbptr[0.. { } // Parse the cookie - let values = jsonRequest.headers["Cookie"]! + let values = jsonRequest.headers["Cookie"] ?? [] #expect(values.count == 1) - let cookie = values[0] - #expect(cookie.starts(with: "foo=")) - return cookie.components(separatedBy: ";").first! + let cookie = values.first + #expect(cookie?.starts(with: "foo=") ?? false) + return cookie?.components(separatedBy: ";").first } // The cookie should be the same From 5a9a5c0a22a13b6b8a60070aa2ea4b86c577dc86 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 10:56:14 +0100 Subject: [PATCH 03/11] Module rename --- Package.swift | 6 +++--- .../AHC => AsyncHTTPClientConformance}/AHC+HTTP.swift | 0 .../Suite.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename Sources/{AHCConformance/AHC => AsyncHTTPClientConformance}/AHC+HTTP.swift (100%) rename Tests/{AHCConformanceTests => AsyncHTTPClientConformanceTests}/Suite.swift (97%) diff --git a/Package.swift b/Package.swift index dc787ec..c6dc796 100644 --- a/Package.swift +++ b/Package.swift @@ -88,7 +88,7 @@ let package = Package( swiftSettings: extraSettings ), .target( - name: "AHCConformance", + name: "AsyncHTTPClientConformance", dependencies: [ "HTTPAPIs", "AsyncStreaming", @@ -162,9 +162,9 @@ let package = Package( swiftSettings: extraSettings ), .testTarget( - name: "AHCConformanceTests", + name: "AsyncHTTPClientConformanceTests", dependencies: [ - "AHCConformance", + "AsyncHTTPClientConformance", "HTTPClientConformance", ] ), diff --git a/Sources/AHCConformance/AHC/AHC+HTTP.swift b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift similarity index 100% rename from Sources/AHCConformance/AHC/AHC+HTTP.swift rename to Sources/AsyncHTTPClientConformance/AHC+HTTP.swift diff --git a/Tests/AHCConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift similarity index 97% rename from Tests/AHCConformanceTests/Suite.swift rename to Tests/AsyncHTTPClientConformanceTests/Suite.swift index e5400ce..5ec2eba 100644 --- a/Tests/AHCConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import AHCConformance +import AsyncHTTPClientConformance import AsyncHTTPClient import HTTPAPIs import HTTPClient From 04828dab6eabbfaa5a46afd0e75532d3df22be30 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 16:31:40 +0100 Subject: [PATCH 04/11] PR review --- .../AsyncHTTPClientConformance/AHC+HTTP.swift | 74 +++++++++++++------ .../HTTPClientConformance.swift | 38 ++++++++++ 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift index 5aa42ca..d205158 100644 --- a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift @@ -46,7 +46,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } public mutating func write( - _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result + _ body: (inout OutputSpan) async throws(Failure) -> Result ) async throws(AsyncStreaming.EitherError) -> Result where Failure: Error { let result: Result do { @@ -71,14 +71,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { // with regards overlapping memory access. var localArray = RigidArray(capacity: 0) swap(&localArray, &self.rigidArray) - unsafe localArray.span.withUnsafeBufferPointer { bufferPtr in - self.byteBuffer.reserveCapacity(bufferPtr.count) - unsafe self.byteBuffer.withUnsafeMutableWritableBytes { byteBufferPtr in - unsafe byteBufferPtr.copyBytes(from: bufferPtr) - } - self.byteBuffer.moveWriterIndex(forwardBy: bufferPtr.count) - } - + unsafe self.byteBuffer.writeBytes(localArray.span.bytes) swap(&localArray, &self.rigidArray) try await self.requestWriter.writeRequestBodyPart(self.byteBuffer) } catch { @@ -101,9 +94,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } public consuming func consumeAndConclude( - body: - nonisolated(nonsending) (consuming sending ResponseBodyReader) async throws(Failure) -> - Return + body: (consuming sending ResponseBodyReader) async throws(Failure) -> Return ) async throws(Failure) -> (Return, HTTPFields?) where Failure: Error { let iterator = self.underlying.makeAsyncIterator() let reader = ResponseBodyReader(underlying: iterator) @@ -125,39 +116,80 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { public typealias ReadFailure = any Error var underlying: HTTPClientResponse.Body.AsyncIterator + var out = RigidArray() + var readerIndex = 0 public mutating func read( maximumCount: Int?, - body: nonisolated(nonsending) (consuming Span) async throws(Failure) -> Return + body: (consuming Span) async throws(Failure) -> Return ) async throws(AsyncStreaming.EitherError) -> Return where Failure: Error { - do { + // if have enough data for the read request available, hand it to the user right away + if let maximumCount, maximumCount <= self.out.count - self.readerIndex { + defer { + self.readerIndex += maximumCount + self.reallocateIfNeeded() + } + return try await body(self.out.span.extracting(self.readerIndex..<(self.readerIndex + maximumCount))) + } + + // we have data remaining in the local buffer. hand that to the user next. + if self.readerIndex < self.out.count { + defer { + self.readerIndex = self.out.count + self.reallocateIfNeeded() + } + return try await body(self.out.span.extracting(self.readerIndex.. { _ in } return try await body(array.span) } - var array = RigidArray() - let capcity = maximumCount != nil ? min(maximumCount!, buffer.readableBytes) : buffer.readableBytes - array.reserveCapacity(capcity) + + let readLength = maximumCount != nil ? min(maximumCount!, buffer.readableBytes) : buffer.readableBytes + self.out.reserveCapacity(self.out.count + buffer.readableBytes) + let alreadyRead = self.out.count unsafe buffer.withUnsafeReadableBytes { rawBufferPtr in let usbptr = unsafe rawBufferPtr.assumingMemoryBound(to: UInt8.self) - unsafe array.append(copying: usbptr[0.. 2 ^ 16 else { + return + } + + let newCapacity = max(self.out.count - self.readerIndex, 2 ^ 16) + + self.out = RigidArray(capacity: newCapacity) { + // this is probably super slow. + for i in self.readerIndex..( request: HTTPRequest, body: consuming HTTPClientRequestBody?, options: RequestOptions, - responseHandler: nonisolated(nonsending) (HTTPResponse, consuming ResponseReader) async throws -> Return + responseHandler: (HTTPResponse, consuming ResponseReader) async throws -> Return ) async throws -> Return { guard let url = request.url else { fatalError() diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index 2269a79..367ec76 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -229,6 +229,7 @@ struct BasicConformanceTests { try await testCancelPreBody() try await testEcho1MBBody() try await testUnderRead() + try await testDripRead() try await testClientSendsEmptyHeaderValue() try await testInfiniteRedirect() try await testHeadWithContentLength() @@ -851,6 +852,43 @@ struct BasicConformanceTests { } } + func testDripRead() async throws { + let client = try await clientFactory() + let request = HTTPRequest( + method: .get, + scheme: "http", + authority: "127.0.0.1:\(port)", + path: "/1mb_body" + ) + + // Read only a single byte from the body. We do not care about the rest of the 1Mb. + try await client.perform( + request: request, + ) { response, responseBodyAndTrailers in + #expect(response.status == .ok) + + let (result, _) = try await responseBodyAndTrailers.consumeAndConclude { reader in + var result = [UInt8]() + var reader = reader + var breakTheLoop = false + while !breakTheLoop { + breakTheLoop = try await reader.read(maximumCount: 1) { bytes in + if bytes.isEmpty { + return true + } else { + precondition(bytes.count == 1) + result.append(bytes[0]) + return false + } + } + } + + return result + } + #expect(result == [UInt8](repeating: UInt8(ascii: "A"), count: 1_000_000)) + } + } + func testHeadWithContentLength() async throws { let client = try await clientFactory() let request = HTTPRequest( From 84be931c3ebecc54de4862cd6d30f68b47ba4b50 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 16:48:58 +0100 Subject: [PATCH 05/11] Revert adding drip test This reverts commit 04828dab6eabbfaa5a46afd0e75532d3df22be30. --- .../HTTPClientConformance.swift | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/Sources/HTTPClientConformance/HTTPClientConformance.swift b/Sources/HTTPClientConformance/HTTPClientConformance.swift index d93c0a7..5a21ff0 100644 --- a/Sources/HTTPClientConformance/HTTPClientConformance.swift +++ b/Sources/HTTPClientConformance/HTTPClientConformance.swift @@ -229,7 +229,6 @@ struct BasicConformanceTests { try await testCancelPreBody() try await testEcho1MBBody() try await testUnderRead() - try await testDripRead() try await testClientSendsEmptyHeaderValue() try await testInfiniteRedirect() try await testHeadWithContentLength() @@ -856,43 +855,6 @@ struct BasicConformanceTests { } } - func testDripRead() async throws { - let client = try await clientFactory() - let request = HTTPRequest( - method: .get, - scheme: "http", - authority: "127.0.0.1:\(port)", - path: "/1mb_body" - ) - - // Read only a single byte from the body. We do not care about the rest of the 1Mb. - try await client.perform( - request: request, - ) { response, responseBodyAndTrailers in - #expect(response.status == .ok) - - let (result, _) = try await responseBodyAndTrailers.consumeAndConclude { reader in - var result = [UInt8]() - var reader = reader - var breakTheLoop = false - while !breakTheLoop { - breakTheLoop = try await reader.read(maximumCount: 1) { bytes in - if bytes.isEmpty { - return true - } else { - precondition(bytes.count == 1) - result.append(bytes[0]) - return false - } - } - } - - return result - } - #expect(result == [UInt8](repeating: UInt8(ascii: "A"), count: 1_000_000)) - } - } - func testHeadWithContentLength() async throws { let client = try await clientFactory() let request = HTTPRequest( From f9fc591edca61ad013a442b0a01b7c9d433fb4fa Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 17:43:35 +0100 Subject: [PATCH 06/11] swift format --- Sources/AsyncHTTPClientConformance/AHC+HTTP.swift | 4 ++-- Tests/AsyncHTTPClientConformanceTests/Suite.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift index d205158..fc92bcb 100644 --- a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift @@ -144,7 +144,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { // we don't have enough data let buffer = try await self.underlying.next(isolation: #isolation) - guard let buffer else { // eof received + guard let buffer else { // eof received let array = InlineArray<0, UInt8> { _ in } return try await body(array.span) } @@ -160,7 +160,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { self.readerIndex = alreadyRead + readLength self.reallocateIfNeeded() } - return try await body(self.out.span.extracting(alreadyRead..<(alreadyRead+readLength))) + return try await body(self.out.span.extracting(alreadyRead..<(alreadyRead + readLength))) } catch let error as Failure { throw .second(error) } catch { diff --git a/Tests/AsyncHTTPClientConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift index 5ec2eba..207ae62 100644 --- a/Tests/AsyncHTTPClientConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import AsyncHTTPClientConformance import AsyncHTTPClient +import AsyncHTTPClientConformance import HTTPAPIs import HTTPClient import HTTPClientConformance From 7e3dfc8844fde9799889cff08f98ba18351346be Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 18:37:01 +0100 Subject: [PATCH 07/11] header fixes. --- Sources/AsyncHTTPClientConformance/AHC+HTTP.swift | 6 +++--- Tests/AsyncHTTPClientConformanceTests/Suite.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift index fc92bcb..6d37384 100644 --- a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift @@ -1,12 +1,12 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the AsyncHTTPClient open source project +// This source file is part of the Swift HTTP API Proposal open source project // -// Copyright (c) 2025 Apple Inc. and the AsyncHTTPClient project authors +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors // // SPDX-License-Identifier: Apache-2.0 // diff --git a/Tests/AsyncHTTPClientConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift index 207ae62..e086504 100644 --- a/Tests/AsyncHTTPClientConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -1,12 +1,12 @@ //===----------------------------------------------------------------------===// // -// This source file is part of the AsyncHTTPClient open source project +// This source file is part of the Swift HTTP API Proposal open source project // -// Copyright (c) 2026 Apple Inc. and the AsyncHTTPClient project authors +// Copyright (c) 2026 Apple Inc. and the Swift HTTP API Proposal project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// See CONTRIBUTORS.txt for the list of Swift HTTP API Proposal project authors // // SPDX-License-Identifier: Apache-2.0 // From 47dd3732781feccbd90b227334624eb43ee72eda Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 4 Mar 2026 18:37:56 +0100 Subject: [PATCH 08/11] fix test method --- Tests/AsyncHTTPClientConformanceTests/Suite.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift index e086504..a137941 100644 --- a/Tests/AsyncHTTPClientConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -29,7 +29,7 @@ import Testing let httpClient = HTTPClient(eventLoopGroup: .singletonMultiThreadedEventLoopGroup, configuration: config) defer { Task { try await httpClient.shutdown() } } - try await runBasicConformanceTests { + try await runConformanceTests { httpClient } } From b81f5e6897ff59e2d48f8baf949a52bef3874dbd Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 5 Mar 2026 09:34:20 +0100 Subject: [PATCH 09/11] Update Tests/AsyncHTTPClientConformanceTests/Suite.swift Co-authored-by: Xyan Bhatnagar --- Tests/AsyncHTTPClientConformanceTests/Suite.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift index a137941..875a4e0 100644 --- a/Tests/AsyncHTTPClientConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -29,7 +29,12 @@ import Testing let httpClient = HTTPClient(eventLoopGroup: .singletonMultiThreadedEventLoopGroup, configuration: config) defer { Task { try await httpClient.shutdown() } } - try await runConformanceTests { + try await runConformanceTests(excluding: [ + // TODO: AHC does not support cookies + .testBasicCookieSetAndUse, + // TODO: AHC does not support caching + .testETag + ]) { httpClient } } From 5f8f31a921f7debee8ec1607bf4720e4b7aaa893 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 5 Mar 2026 12:16:19 +0100 Subject: [PATCH 10/11] swift format --- Tests/AsyncHTTPClientConformanceTests/Suite.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AsyncHTTPClientConformanceTests/Suite.swift b/Tests/AsyncHTTPClientConformanceTests/Suite.swift index 875a4e0..c2df9ca 100644 --- a/Tests/AsyncHTTPClientConformanceTests/Suite.swift +++ b/Tests/AsyncHTTPClientConformanceTests/Suite.swift @@ -33,7 +33,7 @@ import Testing // TODO: AHC does not support cookies .testBasicCookieSetAndUse, // TODO: AHC does not support caching - .testETag + .testETag, ]) { httpClient } From 9dc7d7729739f3618aadd1a601884aadaea08da9 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Fri, 6 Mar 2026 12:28:22 +0100 Subject: [PATCH 11/11] Fix source --- Sources/AsyncHTTPClientConformance/AHC+HTTP.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift index 6d37384..029f881 100644 --- a/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClientConformance/AHC+HTTP.swift @@ -27,7 +27,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { public typealias ResponseConcludingReader = ResponseReader public struct RequestOptions: HTTPClientCapability.RequestOptions { - public init() {} + } public struct RequestBodyWriter: AsyncWriter, ~Copyable { @@ -185,6 +185,10 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } } + public var defaultRequestOptions: RequestOptions { + RequestOptions() + } + public func perform( request: HTTPRequest, body: consuming HTTPClientRequestBody?,