From 73d0974823e3268a5adb3b9d7b659fc75da8d563 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 7 Oct 2026 12:30:42 +0200 Subject: [PATCH 1/2] Fix crash in debug when request body stream finishes after a >= 300 response head (#934) ### Motivation If a server responds with a status >= 300 while we are still uploading the request body, we ask the producer to pause the request body stream. The producer may still finish the stream, because it raced with the pause request. In this case we hit `assert(head.status.code < 300)` in `HTTPRequestStateMachine.requestStreamFinished`, which crashes debug builds. ### Changes - Remove the wrong assertion. Finishing the request body stream after a >= 300 response head is a valid state transition: we send the request `.end` and move into `.endSent`. - Add a reproducer in `HTTPRequestStateMachineTests` that streams a body part, receives a 413 Payload Too Large head and then finishes the request body stream. ### Result A request body stream that finishes after the server responded with a `status >= 300` no longer crashes in debug. Since the request `.end` is on the wire, the connection can be reused after the response ends. --------- Co-authored-by: Eric Rosenberg --- .../HTTPRequestStateMachine.swift | 2 -- .../HTTPRequestStateMachineTests.swift | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift index cef736063..0dd90f2de 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/HTTPRequestStateMachine.swift @@ -376,8 +376,6 @@ struct HTTPRequestStateMachine { .streaming(let expectedBodyLength, let sentBodyBytes, _), .receivingBody(let head, let streamState) ): - assert(head.status.code < 300) - if let expected = expectedBodyLength, expected != sentBodyBytes { let error = HTTPClientError.bodyLengthMismatch self.state = .failed(error) diff --git a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift index b4845005c..ad08c7a44 100644 --- a/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPRequestStateMachineTests.swift @@ -325,6 +325,42 @@ class HTTPRequestStateMachineTests: XCTestCase { XCTAssertEqual(state.channelRead(.end(nil)), .forwardResponseEnd(.requestDone, [], nil)) } + func testRequestBodyStreamFinishedAfterServerSentHeadWithStatus413() { + var state = HTTPRequestStateMachine(isChannelWritable: true) + let requestHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/") + let metadata = RequestFramingMetadata(connectionClose: false, body: .stream) + XCTAssertEqual( + state.startRequest(head: requestHead, metadata: metadata), + .sendRequestHead(requestHead, sendEnd: false) + ) + // Promotes the producer to `.producing`, as the real pipeline does once the head is out. + XCTAssertEqual( + state.headSent(), + .notifyRequestHeadSendSuccessfully(resumeRequestBodyStream: true, startIdleTimer: false) + ) + + // One part is on the wire and the stream is still open. + let part0 = IOData.byteBuffer(ByteBuffer(bytes: 0...3)) + XCTAssertEqual(state.requestStreamPartReceived(part0, promise: nil), .sendBodyPart(part0, nil)) + + // The server rejects the upload without reading the rest of it. The state machine takes + // this in stride and asks us to pause the body stream. + let responseHead = HTTPResponseHead(version: .http1_1, status: .payloadTooLarge) + XCTAssertEqual( + state.channelRead(.head(responseHead)), + .forwardResponseHead(responseHead, pauseRequestBodyStream: true) + ) + + // Pausing is not cancelling, so the producer may still finish. This is the trap: the + // action below is the correct one, and the assert crashes before it can be returned. + XCTAssertEqual( + state.requestStreamFinished(trailers: nil, promise: nil), + .sendRequestEnd(trailers: nil, nil, .none) + ) + + XCTAssertEqual(state.channelRead(.end(nil)), .forwardResponseEnd(.requestDone, [], nil)) + } + func testRequestIsFailedIfRequestBodySizeIsWrongEvenAfterServerRespondedWith200() { var state = HTTPRequestStateMachine(isChannelWritable: true) let requestHead = HTTPRequestHead( From 017115279d09dbfd262a6c678e96d31c90b3b2aa Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Wed, 7 Oct 2026 15:20:57 +0200 Subject: [PATCH 2/2] Lift minimum Swift version to 6.2 (#935) Co-authored-by: Si Beaumont --- Package@swift-6.1.swift | 124 ------------------ README.md | 5 +- ...ientConfiguration+SwiftConfiguration.swift | 2 - .../BidirectionalStreamingTests.swift | 2 +- .../HTTPClientTestUtils.swift | 8 +- .../SwiftConfigurationTests.swift | 2 - 6 files changed, 5 insertions(+), 138 deletions(-) delete mode 100644 Package@swift-6.1.swift diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift deleted file mode 100644 index eb896f89b..000000000 --- a/Package@swift-6.1.swift +++ /dev/null @@ -1,124 +0,0 @@ -// swift-tools-version:6.1 -//===----------------------------------------------------------------------===// -// -// This source file is part of the AsyncHTTPClient open source project -// -// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import PackageDescription - -let strictConcurrencyDevelopment = false - -let strictConcurrencySettings: [SwiftSetting] = { - var initialSettings: [SwiftSetting] = [] - - if strictConcurrencyDevelopment { - // -warnings-as-errors here is a workaround so that IDE-based development can - // get tripped up on -require-explicit-sendable. - initialSettings.append(.unsafeFlags(["-Xfrontend", "-require-explicit-sendable", "-warnings-as-errors"])) - } - - return initialSettings -}() - -let package = Package( - name: "async-http-client", - products: [ - .library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]) - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.100.0"), - .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"), - .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.36.0"), - .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.26.0"), - .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.24.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.7.1"), - .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), - .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"), - ], - targets: [ - .target( - name: "CAsyncHTTPClient", - cSettings: [ - .define("_GNU_SOURCE") - ] - ), - .target( - name: "AsyncHTTPClient", - dependencies: [ - .target(name: "CAsyncHTTPClient"), - .product(name: "NIO", package: "swift-nio"), - .product(name: "NIOTLS", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - .product(name: "NIOHTTPCompression", package: "swift-nio-extras"), - .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "NIOTransportServices", package: "swift-nio-transport-services"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms"), - // Observability support - .product(name: "Logging", package: "swift-log"), - .product(name: "Tracing", package: "swift-distributed-tracing"), - ], - swiftSettings: strictConcurrencySettings - ), - .testTarget( - name: "AsyncHTTPClientTests", - dependencies: [ - .target(name: "AsyncHTTPClient"), - .product(name: "NIOTLS", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "NIOEmbedded", package: "swift-nio"), - .product(name: "NIOFoundationCompat", package: "swift-nio"), - .product(name: "NIOTestUtils", package: "swift-nio"), - .product(name: "NIOSSL", package: "swift-nio-ssl"), - .product(name: "NIOHTTP2", package: "swift-nio-http2"), - .product(name: "NIOSOCKS", package: "swift-nio-extras"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Algorithms", package: "swift-algorithms"), - // Observability support - .product(name: "Logging", package: "swift-log"), - .product(name: "InMemoryLogging", package: "swift-log"), - .product(name: "Tracing", package: "swift-distributed-tracing"), - .product(name: "InMemoryTracing", package: "swift-distributed-tracing"), - ], - resources: [ - .copy("Resources/self_signed_cert.pem"), - .copy("Resources/self_signed_key.pem"), - .copy("Resources/example.com.cert.pem"), - .copy("Resources/example.com.private-key.pem"), - ], - swiftSettings: strictConcurrencySettings - ), - ] -) - -// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // -for target in package.targets { - switch target.type { - case .regular, .test, .executable: - var settings = target.swiftSettings ?? [] - // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md - settings.append(.enableUpcomingFeature("MemberImportVisibility")) - target.swiftSettings = settings - case .macro, .plugin, .system, .binary: - () // not applicable - @unknown default: - () // we don't know what to do here, do nothing - } -} -// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // diff --git a/README.md b/README.md index eab0173f7..428608242 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ Please have a look at [SECURITY.md](SECURITY.md) for AsyncHTTPClient's security ## Supported Versions -The most recent versions of AsyncHTTPClient support Swift 6.1 and newer. The minimum Swift version supported by AsyncHTTPClient releases are detailed below: +The most recent versions of AsyncHTTPClient support Swift 6.2 and newer. The minimum Swift version supported by AsyncHTTPClient releases are detailed below: AsyncHTTPClient | Minimum Swift Version --------------------|---------------------- @@ -320,4 +320,5 @@ AsyncHTTPClient | Minimum Swift Version `1.26.0 ..< 1.27.0` | 5.9 `1.27.0 ..< 1.30.0` | 5.10 `1.30.0 ..< 1.34.0` | 6.0 -`1.34.0 ...` | 6.1 +`1.34.0 ..< 1.36.2` | 6.1 +`1.37.0 ...` | 6.2 diff --git a/Sources/AsyncHTTPClient/HTTPClientConfiguration+SwiftConfiguration.swift b/Sources/AsyncHTTPClient/HTTPClientConfiguration+SwiftConfiguration.swift index 9430689e9..f56b00a58 100644 --- a/Sources/AsyncHTTPClient/HTTPClientConfiguration+SwiftConfiguration.swift +++ b/Sources/AsyncHTTPClient/HTTPClientConfiguration+SwiftConfiguration.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.2) import Configuration import NIOCore import NIOHTTP1 @@ -272,4 +271,3 @@ extension HTTPClient.Authorization { } } } -#endif diff --git a/Tests/AsyncHTTPClientTests/BidirectionalStreamingTests.swift b/Tests/AsyncHTTPClientTests/BidirectionalStreamingTests.swift index 68adda983..0495fc412 100644 --- a/Tests/AsyncHTTPClientTests/BidirectionalStreamingTests.swift +++ b/Tests/AsyncHTTPClientTests/BidirectionalStreamingTests.swift @@ -69,7 +69,7 @@ struct BidirectionalStreamingTests { } } -final class HTTPRequestStreamingChannel: ChannelInboundHandler & AHCTestSendableMetatype { +final class HTTPRequestStreamingChannel: ChannelInboundHandler & SendableMetatype { typealias InboundIn = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index ea80cee6e..6ef37f3a3 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -364,13 +364,7 @@ enum TestTLS { ) } -#if compiler(>=6.2) -typealias AHCTestSendableMetatype = SendableMetatype -#else -typealias AHCTestSendableMetatype = Any -#endif - -internal final class HTTPBin: Sendable +internal final class HTTPBin: Sendable where RequestHandler.InboundIn == HTTPServerRequestPart, RequestHandler.OutboundOut == HTTPServerResponsePart diff --git a/Tests/AsyncHTTPClientTests/SwiftConfigurationTests.swift b/Tests/AsyncHTTPClientTests/SwiftConfigurationTests.swift index e80d49ed8..4dcd3afdb 100644 --- a/Tests/AsyncHTTPClientTests/SwiftConfigurationTests.swift +++ b/Tests/AsyncHTTPClientTests/SwiftConfigurationTests.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.2) import Configuration import Foundation import NIOCore @@ -575,4 +574,3 @@ struct HTTPClientConfigurationPropsTests { } } } -#endif