From 1dec19fed4d9dceb0f4c98ad6874ba7a3542147e Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Mon, 19 Jan 2026 13:15:15 +0100 Subject: [PATCH 01/12] Tests pass Remove debug line --- Package.swift | 7 + .../AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 226 ++++++++++++++++++ .../HTTP APIs/AHC+HTTP_Tests.swift | 82 +++++++ 3 files changed, 315 insertions(+) create mode 100644 Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift create mode 100644 Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift diff --git a/Package.swift b/Package.swift index 330890a89..8f1adae19 100644 --- a/Package.swift +++ b/Package.swift @@ -24,6 +24,9 @@ let strictConcurrencySettings: [SwiftSetting] = { // -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"])) + initialSettings.append(.enableExperimentalFeature("LifetimeDependence")) + initialSettings.append(.enableExperimentalFeature("Lifetimes")) + initialSettings.append(.enableUpcomingFeature("LifetimeDependence")) } return initialSettings @@ -45,6 +48,7 @@ let package = Package( .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"), + .package(path: "../swift-http-client-server-apis"), ], targets: [ .target( @@ -74,6 +78,9 @@ let package = Package( // Observability support .product(name: "Logging", package: "swift-log"), .product(name: "Tracing", package: "swift-distributed-tracing"), + + // HTTP APIs + .product(name: "HTTPAPIs", package: "swift-http-client-server-apis"), ], swiftSettings: strictConcurrencySettings ), diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift new file mode 100644 index 000000000..bbda947ac --- /dev/null +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -0,0 +1,226 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.2) +import HTTPAPIs +import HTTPTypes +import NIOHTTP1 +import Foundation +import NIOCore +import Synchronization +import BasicContainers + +@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, *) +extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { + public typealias RequestConcludingWriter = RequestWriter + public typealias ResponseConcludingReader = ResponseReader + + public struct RequestWriter: ConcludingAsyncWriter, ~Copyable, SendableMetatype { + public typealias Underlying = RequestBodyWriter + public typealias FinalElement = HTTPFields? + + final class Storage: Sendable { + struct StateMachine: ~Copyable { + + enum State: ~Copyable { + case buffer(CheckedContinuation?) + case demand(CheckedContinuation) + case done(HTTPFields?) + } + + var state: State = .buffer(nil) + + init() { + + } + + } + + let stateMutex = Mutex(StateMachine()) + + nonisolated(nonsending) func write( + _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result) async throws(AsyncStreaming.EitherError + ) -> Result where Failure : Error { + + fatalError() +// do { +// +// self.stateMutex.withLock { state in +// +// } +// +// let updated = try await self.buffer.edit { (outputSpan) async throws(Failure) -> Result in +// try await body(&outputSpan) +// } +// +// +// +// return updated +// } catch { +// throw .first(error) +// } + } + + func next() -> ByteBuffer { + fatalError() + } + } + + let storage: Storage + + init() { + self.storage = .init() + } + + public consuming func produceAndConclude( + body: nonisolated(nonsending) (consuming sending HTTPClient.RequestBodyWriter) async throws -> (Return, HTTPFields?) + ) async throws -> Return { + let bodyWriter = RequestBodyWriter(storage: self.storage) + do { + let (ret, fields) = try await body(bodyWriter) + return ret + } catch { + throw error + } + } + } + + public struct RequestBodyWriter: AsyncWriter, ~Copyable { + public typealias WriteElement = UInt8 + public typealias WriteFailure = any Error + + let storage: RequestWriter.Storage + + public mutating func write( + _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result) async throws(AsyncStreaming.EitherError + ) -> Result where Failure : Error { + try await self.storage.write(body) + } + } + + 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 HTTPClient.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) + return (returnValue, nil) + } + + } + + 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) + if let buffer { + var array = RigidArray() + array.reserveCapacity(buffer.readableBytes) + buffer.withUnsafeReadableBytes { rawBufferPtr in + let usbptr = rawBufferPtr.assumingMemoryBound(to: UInt8.self) + array.append(copying: usbptr) + } + return try await body(array.span) + } else { + let array = InlineArray<0, UInt8> { _ in } + 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?, + configuration: HTTPClientConfiguration, + eventHandler: borrowing some HTTPClientEventHandler & ~Copyable & ~Escapable, + responseHandler: nonisolated(nonsending) (HTTPResponse, consuming ResponseReader) async throws -> Return + ) async throws -> Return { + guard let url = request.url else { + fatalError() + } + + 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) + } + + let result = try await withThrowingTaskGroup { taskGroup in + switch body { + case .none: + break + + case .restartable(let handler): + taskGroup.addTask { + let writer = RequestWriter() + try await handler(writer) + } + case .some(.seekable(_)): + fatalError() + } + + 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) { + responseFields[name] = value + } + } + + let response = HTTPResponse( + status: .init(code: Int(ahcResponse.status.code)), + headerFields: responseFields + ) + + return try await responseHandler(response, .init(underlying: ahcResponse.body)) + + } + + return result + } +} + +//private struct ClosureAsyncSequence: AsyncSequence { +// var body: +// +//} + +#endif diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift new file mode 100644 index 000000000..9c5e6813f --- /dev/null +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -0,0 +1,82 @@ +// +// AHC+HTTP_Tests.swift +// async-http-client +// +// Created by Fabian Fett on 02.12.25. +// + +#if compiler(>=6.2) +import NIOCore +import HTTPTypes +import HTTPAPIs +import Testing +import AsyncHTTPClient + +#if canImport(Darwin) +public import Security +#endif + +@Suite +struct AbstractHTTPClientTest { + + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) + struct EventHandler: HTTPClientEventHandler { + func handleRedirection(response: HTTPTypes.HTTPResponse, newRequest: HTTPTypes.HTTPRequest) async throws -> HTTPAPIs.HTTPClientRedirectionAction { + .deliverRedirectionResponse + } + + #if canImport(Darwin) + func handleServerTrust(_ trust: SecTrust) async throws -> HTTPAPIs.HTTPClientTrustResult { + fatalError() + } + #endif + } + + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) + @Test func testExample() async throws { + + let bin = HTTPBin(.http1_1(ssl: false)) + defer { try! bin.shutdown() } + + let client = HTTPClient() + defer { try! client.shutdown().wait() } + + let request = HTTPRequest(method: .get, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/stats") +// let body = HTTPClientRequestBody.restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in +// try await writer.produceAndConclude { writer in +// +// var mwriter = writer +// +// try await mwriter.write { outputSpan in +// outputSpan.append(repeating: UInt8(ascii: "X"), count: 10) +// } +// +// return ((), nil) +// } +// } + + let handler = EventHandler() + + try await client.perform( + request: request, + body: nil, + configuration: .init(), + eventHandler: handler + ) { response, responseReader in + print("status: \(response.status)") + for header in response.headerFields { + print("\(header.name): \(header.value)") + } + + let trailers = try await responseReader.collect(upTo: 1024) { span in + span.withUnsafeBufferPointer { buffer in + print(String(decoding: buffer, as: Unicode.UTF8.self)) + } + } + } + + } + +} + +#endif From b0a7431c90de8f2594d157328f5016b52fc845fe Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 11 Feb 2026 16:40:43 +0100 Subject: [PATCH 02/12] Trailers are working! --- .../HTTPClientRequest+Prepared.swift | 8 + .../AsyncAwait/HTTPClientRequest.swift | 14 ++ .../AsyncAwait/HTTPClientResponse.swift | 8 +- .../AsyncAwait/Transaction+StateMachine.swift | 56 ++++--- .../AsyncAwait/Transaction.swift | 10 +- .../AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 157 ++++++------------ .../HTTP APIs/AHC+HTTP_Tests.swift | 25 +-- .../HTTPClientTestUtils.swift | 10 +- 8 files changed, 131 insertions(+), 157 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift index e57406e2b..ff13495cb 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -101,6 +101,10 @@ extension HTTPClientRequest.Prepared.Body { ) case .byteBuffer(let byteBuffer): self = .byteBuffer(byteBuffer) + #if canImport(HTTPAPIs) + case .httpClientRequestBody: + fatalError("Unimplemented") + #endif } } } @@ -115,6 +119,10 @@ extension RequestBodyLength { self = .known(Int64(buffer.readableBytes)) case .sequence(let length, _, _), .asyncSequence(let length, _): self = length + #if canImport(HTTPAPIs) + case .httpClientRequestBody: + fatalError("Unimplemented") + #endif } } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index dca7de0ef..36638a14c 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -16,6 +16,9 @@ import Algorithms import NIOCore import NIOHTTP1 import NIOSSL +#if canImport(HTTPAPIs) +import HTTPAPIs +#endif @usableFromInline let bagOfBytesToByteBufferConversionChunkSize = 1024 * 1024 * 4 @@ -92,6 +95,10 @@ extension HTTPClientRequest { makeCompleteBody: @Sendable (ByteBufferAllocator) -> ByteBuffer ) case byteBuffer(ByteBuffer) + + #if canImport(HTTPAPIs) + case httpClientRequestBody(Sendable /* HTTPClientRequestBody */) + #endif } @usableFromInline @@ -345,6 +352,9 @@ extension Optional where Wrapped == HTTPClientRequest.Body { case .byteBuffer: return true case .sequence(_, let canBeConsumedMultipleTimes, _): return canBeConsumedMultipleTimes case .asyncSequence: return false + #if canImport(HTTPAPIs) + case .httpClientRequestBody: return false // TODO: I think this should be TRUE + #endif } } } @@ -385,6 +395,10 @@ extension HTTPClientRequest.Body: AsyncSequence { return .init(storage: .byteBuffer(makeCompleteBody(AsyncIterator.allocator))) case .byteBuffer(let byteBuffer): return .init(storage: .byteBuffer(byteBuffer)) + #if canImport(HTTPAPIs) + case .httpClientRequestBody(let body): + fatalError("Unimplemented") + #endif } } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift index 36c1cb36f..1803c9cff 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift @@ -78,6 +78,7 @@ public struct HTTPClientResponse: Sendable { version: HTTPVersion, status: HTTPResponseStatus, headers: HTTPHeaders, + transaction: Transaction, body: TransactionBody, history: [HTTPClientRequestResponse] ) { @@ -88,6 +89,7 @@ public struct HTTPClientResponse: Sendable { body: .init( .transaction( body, + transaction, expectedContentLength: HTTPClientResponse.expectedContentLength( requestMethod: requestMethod, headers: headers, @@ -149,7 +151,7 @@ extension HTTPClientResponse { /// - Returns: the number of bytes collected over time @inlinable public func collect(upTo maxBytes: Int) async throws -> ByteBuffer { switch self.storage { - case .transaction(_, let expectedContentLength): + case .transaction(_, _, let expectedContentLength): if let contentLength = expectedContentLength { if contentLength > maxBytes { throw NIOTooManyBytesError(maxBytes: maxBytes) @@ -199,7 +201,7 @@ typealias TransactionBody = NIOThrowingAsyncSequenceProducer< @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientResponse.Body { @usableFromInline enum Storage: Sendable { - case transaction(TransactionBody, expectedContentLength: Int?) + case transaction(TransactionBody, Transaction, expectedContentLength: Int?) case anyAsyncSequence(AnyAsyncSequence) } } @@ -210,7 +212,7 @@ extension HTTPClientResponse.Body.Storage: AsyncSequence { @inlinable func makeAsyncIterator() -> AsyncIterator { switch self { - case .transaction(let transaction, _): + case .transaction(let transaction, _, _): return .transaction(transaction.makeAsyncIterator()) case .anyAsyncSequence(let anyAsyncSequence): return .anyAsyncSequence(anyAsyncSequence.makeAsyncIterator()) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift index 4128998b9..47161838f 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift @@ -31,7 +31,7 @@ extension Transaction { case queued(CheckedContinuation, HTTPRequestScheduler) case deadlineExceededWhileQueued(CheckedContinuation) case executing(ExecutionContext, RequestStreamState, ResponseStreamState) - case finished(error: Error?) + case finished(Result) } fileprivate enum RequestStreamState: Sendable { @@ -47,7 +47,7 @@ extension Transaction { case waitingForResponseHead // streaming response body. Valid transitions to: finished. case streamingBody(TransactionBody.Source) - case finished + case finished(HTTPHeaders?) } private var state: State @@ -105,11 +105,11 @@ extension Transaction { mutating func fail(_ error: Error) -> FailAction { switch self.state { case .initialized(let continuation): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .failResponseHead(continuation, error, nil, nil, bodyStreamContinuation: nil) case .queued(let continuation, let scheduler): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .failResponseHead(continuation, error, scheduler, nil, bodyStreamContinuation: nil) case .deadlineExceededWhileQueued(let continuation): let realError: Error = { @@ -123,12 +123,12 @@ extension Transaction { } }() - self.state = .finished(error: realError) + self.state = .finished(.failure(realError)) return .failResponseHead(continuation, realError, nil, nil, bodyStreamContinuation: nil) case .executing(let context, let requestStreamState, .waitingForResponseHead): switch requestStreamState { case .paused(continuation: .some(let continuation)): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .failResponseHead( context.continuation, error, @@ -138,7 +138,7 @@ extension Transaction { ) case .requestHeadSent, .endForwarded, .finished, .producing, .paused(continuation: .none): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .failResponseHead( context.continuation, error, @@ -149,7 +149,7 @@ extension Transaction { } case .executing(let context, let requestStreamState, .streamingBody(let source)): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) switch requestStreamState { case .paused(let bodyStreamContinuation): return .failResponseStream( @@ -164,7 +164,7 @@ extension Transaction { case .executing(let context, let requestStreamState, .finished): // an error occured after full response received, but before the full request was sent - self.state = .finished(error: error) + self.state = .finished(.failure(error)) switch requestStreamState { case .paused(let bodyStreamContinuation): if let bodyStreamContinuation { @@ -205,14 +205,14 @@ extension Transaction { return .none case .deadlineExceededWhileQueued(let continuation): let error = HTTPClientError.deadlineExceeded - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .cancelAndFail(executor, continuation, with: error) - case .finished(error: .some): + case .finished(.failure): return .cancel(executor) case .executing, - .finished(error: .none): + .finished(.success): preconditionFailure("Invalid state: \(self.state)") } } @@ -402,8 +402,8 @@ extension Transaction { assertionFailure("Invalid state: \(self.state)") return .failure(HTTPClientError.internalStateFailure()) - case .executing(_, .endForwarded, .finished): - self.state = .finished(error: nil) + case .executing(_, .endForwarded, .finished(let trailers)): + self.state = .finished(.success(trailers)) return .none case .executing(let context, .endForwarded, let responseState): @@ -446,12 +446,12 @@ extension Transaction { self.state = .executing(context, requestState, .streamingBody(body.source)) return .succeedResponseHead(body.sequence, context.continuation) - case .finished(error: .some): + case .finished(.failure): // If the request failed before, we don't need to do anything in response to // receiving the response head. return .none - case .finished(error: .none): + case .finished(.success): preconditionFailure("How can the request be finished without error, before receiving response head?") } } @@ -511,7 +511,7 @@ extension Transaction { case none } - mutating func receiveResponseEnd(_ newChunks: CircularBuffer?) -> ReceiveResponseEndAction { + mutating func receiveResponseEnd(_ newChunks: CircularBuffer?, trailers: HTTPHeaders?) -> ReceiveResponseEndAction { switch self.state { case .initialized, .queued, @@ -524,9 +524,9 @@ extension Transaction { case .executing(let context, let requestState, .streamingBody(let source)): switch requestState { case .finished: - self.state = .finished(error: nil) + self.state = .finished(.success(trailers)) case .paused, .producing, .requestHeadSent, .endForwarded: - self.state = .executing(context, requestState, .finished) + self.state = .executing(context, requestState, .finished(trailers)) } return .finishResponseStream(source, finalBody: newChunks) @@ -540,6 +540,18 @@ extension Transaction { } } + var trailers: HTTPHeaders? { + switch self.state { + case .deadlineExceededWhileQueued, .initialized, .queued, + .executing(_, _, .waitingForResponseHead), + .executing(_, _, .streamingBody), + .finished(.failure): + return nil + case .executing(_, _, .finished(let trailers)), .finished(.success(let trailers)): + return trailers + } + } + mutating func httpResponseStreamTerminated() -> FailAction { switch self.state { case .executing(_, _, .finished), .finished: @@ -565,7 +577,7 @@ extension Transaction { let error = HTTPClientError.deadlineExceeded switch self.state { case .initialized(let continuation): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .cancel( requestContinuation: continuation, scheduler: nil, @@ -583,7 +595,7 @@ extension Transaction { case .executing(let context, let requestStreamState, .waitingForResponseHead): switch requestStreamState { case .paused(continuation: .some(let continuation)): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .cancel( requestContinuation: context.continuation, scheduler: nil, @@ -591,7 +603,7 @@ extension Transaction { bodyStreamContinuation: continuation ) case .requestHeadSent, .endForwarded, .finished, .producing, .paused(continuation: .none): - self.state = .finished(error: error) + self.state = .finished(.failure(error)) return .cancel( requestContinuation: context.continuation, scheduler: nil, diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 30c7c877f..2ff6b959f 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -17,6 +17,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOSSL +import Synchronization import Tracing @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @@ -277,6 +278,7 @@ extension Transaction: HTTPExecutableRequest { version: head.version, status: head.status, headers: head.headers, + transaction: self, body: body, history: [] ) @@ -303,7 +305,7 @@ extension Transaction: HTTPExecutableRequest { func receiveResponseEnd(_ buffer: CircularBuffer?, trailers: HTTPHeaders?) { let receiveResponseEndAction = self.state.withLockedValue { state in - state.receiveResponseEnd(buffer) + state.receiveResponseEnd(buffer, trailers: trailers) } switch receiveResponseEndAction { case .finishResponseStream(let source, let finalResponse): @@ -317,6 +319,12 @@ extension Transaction: HTTPExecutableRequest { } } + var trailers: HTTPHeaders? { + self.state.withLockedValue { + $0.trailers + } + } + func httpResponseStreamTerminated() { let action = self.state.withLockedValue { state in state.httpResponseStreamTerminated() diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index bbda947ac..583140ccd 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -23,89 +23,24 @@ import BasicContainers @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, *) extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { - public typealias RequestConcludingWriter = RequestWriter + public typealias RequestWriter = RequestBodyWriter public typealias ResponseConcludingReader = ResponseReader - public struct RequestWriter: ConcludingAsyncWriter, ~Copyable, SendableMetatype { - public typealias Underlying = RequestBodyWriter - public typealias FinalElement = HTTPFields? - - final class Storage: Sendable { - struct StateMachine: ~Copyable { - - enum State: ~Copyable { - case buffer(CheckedContinuation?) - case demand(CheckedContinuation) - case done(HTTPFields?) - } - - var state: State = .buffer(nil) - - init() { - - } - - } - - let stateMutex = Mutex(StateMachine()) - - nonisolated(nonsending) func write( - _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result) async throws(AsyncStreaming.EitherError - ) -> Result where Failure : Error { - - fatalError() -// do { -// -// self.stateMutex.withLock { state in -// -// } -// -// let updated = try await self.buffer.edit { (outputSpan) async throws(Failure) -> Result in -// try await body(&outputSpan) -// } -// -// -// -// return updated -// } catch { -// throw .first(error) -// } - } - - func next() -> ByteBuffer { - fatalError() - } - } - - let storage: Storage - - init() { - self.storage = .init() - } - - public consuming func produceAndConclude( - body: nonisolated(nonsending) (consuming sending HTTPClient.RequestBodyWriter) async throws -> (Return, HTTPFields?) - ) async throws -> Return { - let bodyWriter = RequestBodyWriter(storage: self.storage) - do { - let (ret, fields) = try await body(bodyWriter) - return ret - } catch { - throw error - } - } + public struct RequestOptions: HTTPClientCapability.RequestOptions { + public init() {} } public struct RequestBodyWriter: AsyncWriter, ~Copyable { public typealias WriteElement = UInt8 public typealias WriteFailure = any Error - let storage: RequestWriter.Storage + let transaction: Transaction public mutating func write( _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result) async throws(AsyncStreaming.EitherError ) -> Result where Failure : Error { - try await self.storage.write(body) +// try await self.storage.write(body) + fatalError() } } @@ -126,7 +61,27 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { let iterator = self.underlying.makeAsyncIterator() let reader = ResponseBodyReader(underlying: iterator) let returnValue = try await body(reader) - return (returnValue, nil) + + let trailers: HTTPFields? + switch underlying.storage { + case .transaction(_, let transaction, _): + if let t = transaction.trailers { + let sequence = t.lazy.compactMap({ + if let name = HTTPField.Name($0.name) { + HTTPField(name: name, value: $0.value) + } else { + nil + } + }) + trailers = HTTPFields(sequence) + } else { + trailers = nil + } + + case .anyAsyncSequence: + trailers = nil + } + return (returnValue, trailers) } } @@ -164,11 +119,10 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } } - public func perform( + public func perform( request: HTTPRequest, - body: consuming HTTPClientRequestBody?, - configuration: HTTPClientConfiguration, - eventHandler: borrowing some HTTPClientEventHandler & ~Copyable & ~Escapable, + body: consuming HTTPClientRequestBody?, + options: HTTPClient.RequestOptions, responseHandler: nonisolated(nonsending) (HTTPResponse, consuming ResponseReader) async throws -> Return ) async throws -> Return { guard let url = request.url else { @@ -181,46 +135,33 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { let sequence = request.headerFields.lazy.map({ ($0.name.rawName, $0.value) }) ahcRequest.headers.add(contentsOf: sequence) } + if let body { + ahcRequest.body = .init(.httpClientRequestBody(body)) + } - let result = try await withThrowingTaskGroup { taskGroup in - switch body { - case .none: - break + let ahcResponse = try await self.execute(ahcRequest, timeout: .seconds(30)) - case .restartable(let handler): - taskGroup.addTask { - let writer = RequestWriter() - try await handler(writer) - } - case .some(.seekable(_)): - fatalError() + var responseFields = HTTPFields() + for (name, value) in ahcResponse.headers { + if let name = HTTPField.Name(name) { + responseFields[name] = value } + } - 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) { - responseFields[name] = value - } - } - - let response = HTTPResponse( - status: .init(code: Int(ahcResponse.status.code)), - headerFields: responseFields - ) - - return try await responseHandler(response, .init(underlying: ahcResponse.body)) + let response = HTTPResponse( + status: .init(code: Int(ahcResponse.status.code)), + headerFields: responseFields + ) + let result: Result + do { + result = .success(try await responseHandler(response, .init(underlying: ahcResponse.body))) + } catch { + result = .failure(error) } - return result + return try result.get() } } -//private struct ClosureAsyncSequence: AsyncSequence { -// var body: -// -//} - #endif diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 9c5e6813f..3dc2e9905 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -12,26 +12,9 @@ import HTTPAPIs import Testing import AsyncHTTPClient -#if canImport(Darwin) -public import Security -#endif - @Suite struct AbstractHTTPClientTest { - @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) - struct EventHandler: HTTPClientEventHandler { - func handleRedirection(response: HTTPTypes.HTTPResponse, newRequest: HTTPTypes.HTTPRequest) async throws -> HTTPAPIs.HTTPClientRedirectionAction { - .deliverRedirectionResponse - } - - #if canImport(Darwin) - func handleServerTrust(_ trust: SecTrust) async throws -> HTTPAPIs.HTTPClientTrustResult { - fatalError() - } - #endif - } - @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) @Test func testExample() async throws { @@ -41,7 +24,7 @@ struct AbstractHTTPClientTest { let client = HTTPClient() defer { try! client.shutdown().wait() } - let request = HTTPRequest(method: .get, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/stats") + let request = HTTPRequest(method: .get, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/trailers") // let body = HTTPClientRequestBody.restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in // try await writer.produceAndConclude { writer in // @@ -55,13 +38,11 @@ struct AbstractHTTPClientTest { // } // } - let handler = EventHandler() try await client.perform( request: request, body: nil, - configuration: .init(), - eventHandler: handler + options: .init(), ) { response, responseReader in print("status: \(response.status)") for header in response.headerFields { @@ -73,6 +54,8 @@ struct AbstractHTTPClientTest { print(String(decoding: buffer, as: Unicode.UTF8.self)) } } + + print(trailers) } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 689b4358e..0900868a9 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -786,16 +786,19 @@ internal struct HTTPResponseBuilder { var body: ByteBuffer? var requestBodyByteCount: Int let responseBodyIsRequestBodyByteCount: Bool + let trailers: HTTPHeaders? init( _ version: HTTPVersion = HTTPVersion(major: 1, minor: 1), status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders(), - responseBodyIsRequestBodyByteCount: Bool = false + responseBodyIsRequestBodyByteCount: Bool = false, + trailers: HTTPHeaders? = nil ) { self.head = HTTPResponseHead(version: version, status: status, headers: headers) self.requestBodyByteCount = 0 self.responseBodyIsRequestBodyByteCount = responseBodyIsRequestBodyByteCount + self.trailers = trailers } mutating func add(_ part: ByteBuffer) { @@ -963,6 +966,9 @@ internal final class HTTPBinHandler: ChannelInboundHandler { } self.resps.append(HTTPResponseBuilder(status: .ok)) return + case "/trailers": + self.resps.append(HTTPResponseBuilder(status: .ok, trailers: ["hello": "world"])) + return case "/stats": var body = context.channel.allocator.buffer(capacity: 1) body.writeString("Just some stats mate.") @@ -1133,7 +1139,7 @@ internal final class HTTPBinHandler: ChannelInboundHandler { return } - context.writeAndFlush(self.wrapOutboundOut(.end(nil))).assumeIsolated().whenComplete { result in + context.writeAndFlush(self.wrapOutboundOut(.end(response.trailers))).assumeIsolated().whenComplete { result in self.isServingRequest = false switch result { case .success: From e214892ecdacaf98f9813195d6ffa42855876a62 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 12 Feb 2026 12:07:22 +0100 Subject: [PATCH 03/12] Sending a payload is working! --- .../HTTPClientRequest+Prepared.swift | 15 ++- .../AsyncAwait/HTTPClientRequest.swift | 5 +- .../AsyncAwait/Transaction.swift | 21 +-- .../AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 123 +++++++++++++----- .../HTTP APIs/AHC+HTTP_Tests.swift | 58 ++++++--- .../HTTPClientRequestTests.swift | 3 + .../HTTPClientTestUtils.swift | 4 +- 7 files changed, 166 insertions(+), 63 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift index ff13495cb..f0894603c 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -17,6 +17,9 @@ import NIOCore import NIOHTTP1 import NIOSSL import ServiceContextModule +#if canImport(HTTPAPIs) +import HTTPAPIs +#endif import struct Foundation.URL @@ -34,6 +37,10 @@ extension HTTPClientRequest { makeCompleteBody: @Sendable (ByteBufferAllocator) -> ByteBuffer ) case byteBuffer(ByteBuffer) + + #if canImport(HTTPAPIs) + case httpClientRequestBody(RequestBodyLength, AsyncStream.Continuation) + #endif } var url: URL @@ -102,8 +109,8 @@ extension HTTPClientRequest.Prepared.Body { case .byteBuffer(let byteBuffer): self = .byteBuffer(byteBuffer) #if canImport(HTTPAPIs) - case .httpClientRequestBody: - fatalError("Unimplemented") + case .httpClientRequestBody(let lenght, let requestBody): + self = .httpClientRequestBody(lenght, requestBody) #endif } } @@ -120,8 +127,8 @@ extension RequestBodyLength { case .sequence(let length, _, _), .asyncSequence(let length, _): self = length #if canImport(HTTPAPIs) - case .httpClientRequestBody: - fatalError("Unimplemented") + case .httpClientRequestBody(let length, _): + self = length #endif } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index 36638a14c..ac80898b8 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -97,7 +97,10 @@ extension HTTPClientRequest { case byteBuffer(ByteBuffer) #if canImport(HTTPAPIs) - case httpClientRequestBody(Sendable /* HTTPClientRequestBody */) + case httpClientRequestBody( + length: RequestBodyLength, + startUpload: AsyncStream.Continuation + ) #endif } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift index 2ff6b959f..e12133707 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift @@ -74,7 +74,7 @@ final class Transaction: return } - self.requestBodyStreamFinished() + self.requestBodyStreamFinished(trailers: nil) } private func continueRequestBodyStream( @@ -95,7 +95,7 @@ final class Transaction: } } - self.requestBodyStreamFinished() + self.requestBodyStreamFinished(trailers: nil) } catch { // The only chance of reaching this catch block, is an error thrown in the `next` // call above. @@ -106,7 +106,7 @@ final class Transaction: struct BreakTheWriteLoopError: Swift.Error {} - private func writeRequestBodyPart(_ part: ByteBuffer) async throws { + func writeRequestBodyPart(_ part: ByteBuffer) async throws { let action = self.state.withLockedValue { state in state.writeNextRequestPart() } @@ -147,7 +147,7 @@ final class Transaction: } } - private func requestBodyStreamFinished() { + func requestBodyStreamFinished(trailers: HTTPHeaders?) { let finishAction = self.state.withLockedValue { state in state.finishRequestBodyStream() } @@ -158,7 +158,7 @@ final class Transaction: break case .forwardStreamFinished(let executor): - executor.finishRequestBodyStream(trailers: nil, request: self, promise: nil) + executor.finishRequestBodyStream(trailers: trailers, request: self, promise: nil) } return } @@ -229,12 +229,17 @@ extension Transaction: HTTPExecutableRequest { case .byteBuffer(let byteBuffer): self.writeOnceAndOneTimeOnly(byteBuffer: byteBuffer) - case .none: - break - case .sequence(_, _, let create): let byteBuffer = create(allocator) self.writeOnceAndOneTimeOnly(byteBuffer: byteBuffer) + + #if canImport(HTTPAPIs) + case .httpClientRequestBody(_, let continuation): + continuation.yield(self) + #endif + + case .none: + break } case .resumeStream(let continuation): diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index 583140ccd..84298596b 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -35,15 +35,54 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { public typealias WriteFailure = any Error let transaction: Transaction + var byteBuffer: ByteBuffer + var rigidArray: RigidArray + + init(transaction: Transaction) { + self.transaction = transaction + 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 { -// try await self.storage.write(body) - fatalError() + _ body: nonisolated(nonsending) (inout OutputSpan) async throws(Failure) -> Result + ) async throws(AsyncStreaming.EitherError) -> Result where Failure : Error { + let result: Result + do { + self.rigidArray.reserveCapacity(1024) + result = try await self.rigidArray.append(count: 1024) { (span) async throws(Failure) -> Result in + try await body(&span) + } + } 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) + localArray.span.withUnsafeBufferPointer { bufferPtr in + self.byteBuffer.withUnsafeMutableWritableBytes { byteBufferPtr in + byteBufferPtr.copyBytes(from: bufferPtr) + } + self.byteBuffer.moveWriterIndex(forwardBy: bufferPtr.count) + } + + swap(&localArray, &self.rigidArray) + try await self.transaction.writeRequestBodyPart(self.byteBuffer) + } catch { + throw .first(error) + } + + return result } } + public struct ResponseReader: ConcludingAsyncReader { public typealias Underlying = ResponseBodyReader @@ -129,38 +168,64 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { fatalError() } - 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 { - ahcRequest.body = .init(.httpClientRequestBody(body)) - } + var result: Result? + await withTaskGroup(of: Void.self) { taskGroup in - let ahcResponse = try await self.execute(ahcRequest, timeout: .seconds(30)) + 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 { + let length = body.knownLength.map { RequestBodyLength.known($0) } ?? .unknown + let (asyncStream, startUploadContinuation) = AsyncStream.makeStream(of: Transaction.self) + + taskGroup.addTask { + // TODO: We might want to allow multiple body restarts here. + + for await transaction in asyncStream { + do { + let writer = RequestWriter(transaction: transaction) + 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 + } + transaction.requestBodyStreamFinished(trailers: trailers) + break // the loop + } catch { + fatalError("TODO: Better error handling here: \(error)") + } + } + } - var responseFields = HTTPFields() - for (name, value) in ahcResponse.headers { - if let name = HTTPField.Name(name) { - responseFields[name] = value + ahcRequest.body = .init(.httpClientRequestBody(length: length, startUpload: startUploadContinuation)) } - } - let response = HTTPResponse( - status: .init(code: Int(ahcResponse.status.code)), - headerFields: responseFields - ) + 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) { + responseFields[name] = value + } + } - let result: Result - do { - result = .success(try await responseHandler(response, .init(underlying: ahcResponse.body))) - } catch { - result = .failure(error) + 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() + return try result!.get() } } diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 3dc2e9905..4d2c830c5 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -16,30 +16,14 @@ import AsyncHTTPClient struct AbstractHTTPClientTest { @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) - @Test func testExample() async throws { + @Test func testGet() async throws { let bin = HTTPBin(.http1_1(ssl: false)) defer { try! bin.shutdown() } - let client = HTTPClient() - defer { try! client.shutdown().wait() } + let request = HTTPRequest(method: .post, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/trailers") - let request = HTTPRequest(method: .get, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/trailers") -// let body = HTTPClientRequestBody.restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in -// try await writer.produceAndConclude { writer in -// -// var mwriter = writer -// -// try await mwriter.write { outputSpan in -// outputSpan.append(repeating: UInt8(ascii: "X"), count: 10) -// } -// -// return ((), nil) -// } -// } - - - try await client.perform( + try await HTTPClient.shared.perform( request: request, body: nil, options: .init(), @@ -60,6 +44,42 @@ struct AbstractHTTPClientTest { } + @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) + @Test func testPost() async throws { + + let bin = HTTPBin(.http1_1(ssl: false)) { _ in HTTPEchoHandler() } + defer { try! bin.shutdown() } + + let request = HTTPRequest(method: .post, scheme: "http", authority: "127.0.0.1:\(bin.port)", path: "/") + + try await HTTPClient.shared.perform( + request: request, + body: .restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in + var mwriter = writer + + try await mwriter.write { outputSpan in + outputSpan.append(repeating: UInt8(ascii: "X"), count: 10) + } + + return [.init("foo")!: "bar"] + }, + options: .init(), + ) { response, responseReader in + print("status: \(response.status)") + for header in response.headerFields { + print("\(header.name): \(header.value)") + } + + let trailers = try await responseReader.collect(upTo: 1024) { span in + span.withUnsafeBufferPointer { buffer in + print(String(decoding: buffer, as: Unicode.UTF8.self)) + } + } + + print(trailers) + } + } + } #endif diff --git a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift index 54467aab7..9208352ef 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift @@ -782,6 +782,9 @@ extension Optional where Wrapped == HTTPClientRequest.Prepared.Body { ) } return accumulatedBuffer + + case .httpClientRequestBody: + fatalError("TODO: Unimplemented") } } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 0900868a9..060b8afbc 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -1470,8 +1470,8 @@ class HTTPEchoHandler: ChannelInboundHandler { ) case .body(let bytes): context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(bytes))), promise: nil) - case .end: - context.writeAndFlush(self.wrapOutboundOut(.end(nil))).assumeIsolated().whenSuccess { + case .end(let trailers): + context.writeAndFlush(self.wrapOutboundOut(.end(trailers))).assumeIsolated().whenSuccess { context.close(promise: nil) } } From 06619de90802762ce1c2845613eb4483f078cccd Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 12 Feb 2026 12:22:39 +0100 Subject: [PATCH 04/12] More post stuff --- .../AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 1 + .../HTTP APIs/AHC+HTTP_Tests.swift | 31 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index 84298596b..f593efdf3 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -50,6 +50,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { ) async throws(AsyncStreaming.EitherError) -> Result where Failure : Error { let result: Result do { + 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) diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 4d2c830c5..5aa123eb2 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -57,11 +57,13 @@ struct AbstractHTTPClientTest { body: .restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in var mwriter = writer - try await mwriter.write { outputSpan in - outputSpan.append(repeating: UInt8(ascii: "X"), count: 10) + for i in 0..<100 { + try await mwriter.write { outputSpan in + outputSpan.append("\(i)\n".utf8) + } } - return [.init("foo")!: "bar"] + return [HTTPField.Name("foo")!: "bar"] }, options: .init(), ) { response, responseReader in @@ -70,15 +72,32 @@ struct AbstractHTTPClientTest { print("\(header.name): \(header.value)") } - let trailers = try await responseReader.collect(upTo: 1024) { span in - span.withUnsafeBufferPointer { buffer in - print(String(decoding: buffer, as: Unicode.UTF8.self)) + let trailers = try await responseReader.consumeAndConclude { bodyReader in + var bodyReader = bodyReader + var `continue` = true + while `continue` { + try await bodyReader.read(maximumCount: 1024) { span in + if span.count == 0 { `continue` = false } + + span.withUnsafeBufferPointer { buffer in + print(String(decoding: buffer, as: Unicode.UTF8.self), terminator: "") + } + } } } print(trailers) } } +} + +extension OutputSpan { + + mutating func append(_ sequence: some Sequence) { + for element in sequence { + self.append(element) + } + } } From c4eacee92be567c251d8a640a6d9b5654ce4c37b Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 12 Feb 2026 13:00:58 +0100 Subject: [PATCH 05/12] Count cars demo --- .../HTTP APIs/AHC+HTTP_Tests.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 5aa123eb2..6cfe8f74d 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -45,7 +45,7 @@ struct AbstractHTTPClientTest { } @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) - @Test func testPost() async throws { + @Test func testEcho() async throws { let bin = HTTPBin(.http1_1(ssl: false)) { _ in HTTPEchoHandler() } defer { try! bin.shutdown() } @@ -57,13 +57,18 @@ struct AbstractHTTPClientTest { body: .restartable { (writer: consuming AsyncHTTPClient.HTTPClient.RequestWriter) in var mwriter = writer - for i in 0..<100 { + for i in 1...10 { try await mwriter.write { outputSpan in - outputSpan.append("\(i)\n".utf8) + if i == 1 { + outputSpan.append("\(i) car\n".utf8) + } else { + outputSpan.append("\(i) cars\n".utf8) + } } + try await Task.sleep(for: .milliseconds(400)) } - return [HTTPField.Name("foo")!: "bar"] + return [HTTPField.Name("status")!: "Look Mum, I am done counting!"] }, options: .init(), ) { response, responseReader in @@ -79,14 +84,15 @@ struct AbstractHTTPClientTest { try await bodyReader.read(maximumCount: 1024) { span in if span.count == 0 { `continue` = false } + // Span does not conform to Collection span.withUnsafeBufferPointer { buffer in - print(String(decoding: buffer, as: Unicode.UTF8.self), terminator: "") + print(String(decoding: buffer, as: Unicode.UTF8.self)) } } } } - print(trailers) + print("Trailers: \(trailers)") } } } From 10cc2b4053bda7a75eeed9cae5360f8fb078b77d Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 12 Feb 2026 14:00:44 +0100 Subject: [PATCH 06/12] Further small ajdustments --- Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index f593efdf3..4b21cc779 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -50,6 +50,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { ) 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 @@ -178,6 +179,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { let sequence = request.headerFields.lazy.map({ ($0.name.rawName, $0.value) }) ahcRequest.headers.add(contentsOf: sequence) } + if let body { let length = body.knownLength.map { RequestBodyLength.known($0) } ?? .unknown let (asyncStream, startUploadContinuation) = AsyncStream.makeStream(of: Transaction.self) @@ -196,8 +198,10 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } transaction.requestBodyStreamFinished(trailers: trailers) break // the loop - } catch { - fatalError("TODO: Better error handling here: \(error)") + } catch let error { + // if we fail because the user throws in upload, we have to cancel the + // upload and fail the request I guess. + transaction.fail(error) } } } From 8c3516bcbf29fc79f78a9802b45ec2c3878b39e0 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 18 Feb 2026 14:20:44 +0100 Subject: [PATCH 07/12] Hide feature behind experimental trait... --- Package.swift | 30 +++++++++++- .../AsyncAwait/HTTPClientRequest.swift | 2 +- Tests/ConformanceSuite/Suite.swift | 48 +++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 Tests/ConformanceSuite/Suite.swift diff --git a/Package.swift b/Package.swift index 8f1adae19..bf85a44a6 100644 --- a/Package.swift +++ b/Package.swift @@ -37,6 +37,14 @@ let package = Package( products: [ .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) ], + traits: [ + .trait( + name: "ExperimentalHTTPAPIsSupport", + description: """ + Enables conformance to the HTTPAPIs HTTPClient protocol. This is potentially source breaking. + """ + ), + ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), @@ -48,7 +56,10 @@ let package = Package( .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"), - .package(path: "../swift-http-client-server-apis"), + .package( + url: "https://github.com/apple/swift-http-api-proposal.git", + revision: "31080da9446b9e2c39f84aa955fef2ccbb7549f2", + ), ], targets: [ .target( @@ -80,7 +91,11 @@ let package = Package( .product(name: "Tracing", package: "swift-distributed-tracing"), // HTTP APIs - .product(name: "HTTPAPIs", package: "swift-http-client-server-apis"), + .product( + name: "HTTPAPIs", + package: "swift-http-api-proposal", + condition: .when(traits: ["ExperimentalHTTPAPIsSupport"]) + ), ], swiftSettings: strictConcurrencySettings ), @@ -114,6 +129,17 @@ let package = Package( ], swiftSettings: strictConcurrencySettings ), + .testTarget( + name: "ConformanceSuite", + dependencies: [ + "AsyncHTTPClient", + .product( + name: "HTTPClientConformance", + package: "swift-http-api-proposal", + condition: .when(traits: ["ExperimentalHTTPAPIsSupport"]) + ), + ] + ), ] ) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index ac80898b8..513dd8ed7 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -399,7 +399,7 @@ extension HTTPClientRequest.Body: AsyncSequence { case .byteBuffer(let byteBuffer): return .init(storage: .byteBuffer(byteBuffer)) #if canImport(HTTPAPIs) - case .httpClientRequestBody(let body): + case .httpClientRequestBody: fatalError("Unimplemented") #endif } diff --git a/Tests/ConformanceSuite/Suite.swift b/Tests/ConformanceSuite/Suite.swift new file mode 100644 index 000000000..e6c6f5546 --- /dev/null +++ b/Tests/ConformanceSuite/Suite.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// 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 AsyncHTTPClient +import HTTPAPIs +import HTTPClient +import HTTPClientConformance +import Testing +internal import NIOPosix + +@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 + let httpClient = HTTPClient(eventLoopGroup: .singletonMultiThreadedEventLoopGroup, configuration: config) + defer { Task { try await httpClient.shutdown() } } + + try await runAllConformanceTests { + return httpClient + } + } +} + +@available(macOS 26.2, *) +extension AsyncHTTPClient.HTTPClient.RequestOptions: HTTPClientCapability.RedirectionHandler { + @available(macOS 26.2, *) + public var redirectionHandler: (any HTTPClientRedirectionHandler)? { + get { + nil + } + set { + + } + } +} From 6584afb56e9e021baa24bbc995ca64161893808b Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 18 Feb 2026 14:23:10 +0100 Subject: [PATCH 08/12] Cleanup --- .../HTTP APIs/AHC+HTTP_Tests.swift | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 6cfe8f74d..454d38f6d 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -5,7 +5,7 @@ // Created by Fabian Fett on 02.12.25. // -#if compiler(>=6.2) +#if compiler(>=6.2) && $ExperimentalHTTPAPIsSupport import NIOCore import HTTPTypes import HTTPAPIs @@ -72,11 +72,6 @@ struct AbstractHTTPClientTest { }, options: .init(), ) { response, responseReader in - print("status: \(response.status)") - for header in response.headerFields { - print("\(header.name): \(header.value)") - } - let trailers = try await responseReader.consumeAndConclude { bodyReader in var bodyReader = bodyReader var `continue` = true @@ -91,20 +86,16 @@ struct AbstractHTTPClientTest { } } } - - print("Trailers: \(trailers)") } } } extension OutputSpan { - mutating func append(_ sequence: some Sequence) { for element in sequence { self.append(element) } } - } #endif From e636653ecf0f8b5563f373afd59602507a2f685d Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 18 Feb 2026 14:45:02 +0100 Subject: [PATCH 09/12] Swift format --- .../HTTPClientRequest+Prepared.swift | 5 ++-- .../AsyncAwait/HTTPClientRequest.swift | 3 +- .../AsyncAwait/Transaction+StateMachine.swift | 5 +++- .../AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 28 ++++++++++--------- .../HTTP APIs/AHC+HTTP_Tests.swift | 13 +++++++-- .../HTTPClientTestUtils.swift | 3 +- Tests/ConformanceSuite/Suite.swift | 4 +-- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift index f0894603c..650a1a0fc 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift @@ -17,12 +17,13 @@ import NIOCore import NIOHTTP1 import NIOSSL import ServiceContextModule + +import struct Foundation.URL + #if canImport(HTTPAPIs) import HTTPAPIs #endif -import struct Foundation.URL - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension HTTPClientRequest { struct Prepared: Sendable { diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift index 513dd8ed7..af88030b5 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest.swift @@ -16,6 +16,7 @@ import Algorithms import NIOCore import NIOHTTP1 import NIOSSL + #if canImport(HTTPAPIs) import HTTPAPIs #endif @@ -356,7 +357,7 @@ extension Optional where Wrapped == HTTPClientRequest.Body { case .sequence(_, let canBeConsumedMultipleTimes, _): return canBeConsumedMultipleTimes case .asyncSequence: return false #if canImport(HTTPAPIs) - case .httpClientRequestBody: return false // TODO: I think this should be TRUE + case .httpClientRequestBody: return false // TODO: I think this should be TRUE #endif } } diff --git a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift index 47161838f..01f01d43c 100644 --- a/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift +++ b/Sources/AsyncHTTPClient/AsyncAwait/Transaction+StateMachine.swift @@ -511,7 +511,10 @@ extension Transaction { case none } - mutating func receiveResponseEnd(_ newChunks: CircularBuffer?, trailers: HTTPHeaders?) -> ReceiveResponseEndAction { + mutating func receiveResponseEnd( + _ newChunks: CircularBuffer?, + trailers: HTTPHeaders? + ) -> ReceiveResponseEndAction { switch self.state { case .initialized, .queued, diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index 4b21cc779..8ab99dd48 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -41,13 +41,13 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { init(transaction: Transaction) { self.transaction = transaction self.byteBuffer = ByteBuffer() - self.byteBuffer.reserveCapacity(2^16) - self.rigidArray = RigidArray(capacity: 2^16) // ~ 65k bytes + 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 { + ) async throws(AsyncStreaming.EitherError) -> Result where Failure: Error { let result: Result do { // TODO: rigidArray needs a clear all @@ -84,7 +84,6 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } } - public struct ResponseReader: ConcludingAsyncReader { public typealias Underlying = ResponseBodyReader @@ -97,8 +96,10 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { } public consuming func consumeAndConclude( - body: nonisolated(nonsending) (consuming sending HTTPClient.ResponseBodyReader) async throws(Failure) -> Return - ) async throws(Failure) -> (Return, HTTPFields?) where Failure : Error { + body: + nonisolated(nonsending) (consuming sending HTTPClient.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) @@ -136,7 +137,7 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { public mutating func read( maximumCount: Int?, body: nonisolated(nonsending) (consuming Span) async throws(Failure) -> Return - ) async throws(AsyncStreaming.EitherError) -> Return where Failure : Error { + ) async throws(AsyncStreaming.EitherError) -> Return where Failure: Error { do { let buffer = try await self.underlying.next(isolation: #isolation) @@ -191,13 +192,14 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { do { let writer = RequestWriter(transaction: transaction) 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 - } + let trailers: HTTPHeaders? = + if let trailers = maybeTrailers { + HTTPHeaders(.init(trailers.lazy.map({ ($0.name.rawName, $0.value) }))) + } else { + nil + } transaction.requestBodyStreamFinished(trailers: trailers) - break // the loop + 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. diff --git a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift index 454d38f6d..4299502f4 100644 --- a/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP APIs/AHC+HTTP_Tests.swift @@ -1,9 +1,16 @@ +//===----------------------------------------------------------------------===// // -// AHC+HTTP_Tests.swift -// async-http-client +// This source file is part of the AsyncHTTPClient open source project // -// Created by Fabian Fett on 02.12.25. +// 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 +// +//===----------------------------------------------------------------------===// #if compiler(>=6.2) && $ExperimentalHTTPAPIsSupport import NIOCore diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 060b8afbc..fe2f3b978 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -1139,7 +1139,8 @@ internal final class HTTPBinHandler: ChannelInboundHandler { return } - context.writeAndFlush(self.wrapOutboundOut(.end(response.trailers))).assumeIsolated().whenComplete { result in + context.writeAndFlush(self.wrapOutboundOut(.end(response.trailers))).assumeIsolated().whenComplete { + result in self.isServingRequest = false switch result { case .success: diff --git a/Tests/ConformanceSuite/Suite.swift b/Tests/ConformanceSuite/Suite.swift index e6c6f5546..1a38b1aed 100644 --- a/Tests/ConformanceSuite/Suite.swift +++ b/Tests/ConformanceSuite/Suite.swift @@ -16,8 +16,8 @@ import AsyncHTTPClient import HTTPAPIs import HTTPClient import HTTPClientConformance -import Testing internal import NIOPosix +import Testing @Suite struct AsyncHTTPClientTests { @available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *) @@ -29,7 +29,7 @@ internal import NIOPosix defer { Task { try await httpClient.shutdown() } } try await runAllConformanceTests { - return httpClient + httpClient } } } From b0e0847f2ca0526ac0e187de445e2b51515b84b7 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 18 Feb 2026 14:49:37 +0100 Subject: [PATCH 10/12] swift format 2 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index bf85a44a6..07bd8c23a 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,7 @@ let package = Package( description: """ Enables conformance to the HTTPAPIs HTTPClient protocol. This is potentially source breaking. """ - ), + ) ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), From 209722da538511f7c8f701dbe5b5d0ba7a23719f Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 25 Feb 2026 16:18:09 +0100 Subject: [PATCH 11/12] Make CI green?! --- Package.swift | 11 ++++------- Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 2 +- .../AsyncHTTPClientTests/HTTPClientRequestTests.swift | 2 ++ Tests/ConformanceSuite/Suite.swift | 2 ++ 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index 07bd8c23a..0387b7eed 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,8 @@ let package = Package( description: """ Enables conformance to the HTTPAPIs HTTPClient protocol. This is potentially source breaking. """ - ) + ), + .default(enabledTraits: ["ExperimentalHTTPAPIsSupport"]), // remove before MERGE! ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), @@ -58,7 +59,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"), .package( url: "https://github.com/apple/swift-http-api-proposal.git", - revision: "31080da9446b9e2c39f84aa955fef2ccbb7549f2", + revision: "79028bea099d390935790d5d8884a61eabf448a5", ), ], targets: [ @@ -91,11 +92,7 @@ let package = Package( .product(name: "Tracing", package: "swift-distributed-tracing"), // HTTP APIs - .product( - name: "HTTPAPIs", - package: "swift-http-api-proposal", - condition: .when(traits: ["ExperimentalHTTPAPIsSupport"]) - ), + .product(name: "HTTPAPIs", package: "swift-http-api-proposal"), ], swiftSettings: strictConcurrencySettings ), diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index 8ab99dd48..65154f0df 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.2) +#if compiler(>=6.2) && canImport(HTTPAPIs) import HTTPAPIs import HTTPTypes import NIOHTTP1 diff --git a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift index 9208352ef..54d9e0808 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientRequestTests.swift @@ -783,8 +783,10 @@ extension Optional where Wrapped == HTTPClientRequest.Prepared.Body { } return accumulatedBuffer + #if canImport(HTTPAPIs) case .httpClientRequestBody: fatalError("TODO: Unimplemented") + #endif } } } diff --git a/Tests/ConformanceSuite/Suite.swift b/Tests/ConformanceSuite/Suite.swift index 1a38b1aed..1254a924c 100644 --- a/Tests/ConformanceSuite/Suite.swift +++ b/Tests/ConformanceSuite/Suite.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +#if $ExperimentalHTTPAPIsSupport import AsyncHTTPClient import HTTPAPIs import HTTPClient @@ -46,3 +47,4 @@ extension AsyncHTTPClient.HTTPClient.RequestOptions: HTTPClientCapability.Redire } } } +#endif From 58d9e2172d39c4c7639d2d843f796efae73f3730 Mon Sep 17 00:00:00 2001 From: Xyan Bhatnagar Date: Wed, 25 Feb 2026 09:29:38 -0800 Subject: [PATCH 12/12] Fix bug with response header creation (#892) --- Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift index 65154f0df..b1c101b9c 100644 --- a/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift +++ b/Sources/AsyncHTTPClient/HTTP APIs/AHC+HTTP.swift @@ -217,7 +217,8 @@ extension AsyncHTTPClient.HTTPClient: HTTPAPIs.HTTPClient { var responseFields = HTTPFields() for (name, value) in ahcResponse.headers { if let name = HTTPField.Name(name) { - responseFields[name] = value + // Add a new header field + responseFields.append(.init(name: name, value: value)) } }