From ad4f7eaec53db3e2536e9b820b9d49fe871362a0 Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Mon, 8 Apr 2024 20:52:34 +0300
Subject: [PATCH 1/8] Some handy extensions to work with HTTPClientResponse
 body

---
 .../AsyncAwait/HTTPClientResponse.swift             | 13 +++++++++++++
 Sources/AsyncHTTPClient/FoundationExtensions.swift  | 11 +++++++++++
 2 files changed, 24 insertions(+)

diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
index ee7f11592..4476c90c4 100644
--- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
+++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
@@ -138,6 +138,19 @@ extension HTTPClientResponse {
     }
 }
 
+@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+extension HTTPClientResponse {
+	/// Response body as `ByteBuffer`.
+	public var bytes: ByteBuffer {
+		get async throws {
+			let expectedBytes = headers
+				.first(name: "content-length")
+				.flatMap(Int.init) ?? 1024 * 1024
+			return try await body.collect(upTo: expectedBytes)
+		}
+	}
+}
+
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 @usableFromInline
 typealias TransactionBody = NIOThrowingAsyncSequenceProducer<
diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift
index 545da756b..ca27f2459 100644
--- a/Sources/AsyncHTTPClient/FoundationExtensions.swift
+++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift
@@ -64,3 +64,14 @@ extension HTTPClient.Body {
         return self.bytes(data)
     }
 }
+
+@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
+extension HTTPClientResponse {
+	/// Response body as `Data`.
+	public var data: Data? {
+		get async throws {
+			var bytes = try await bytes
+			return bytes.readData(length: bytes.readableBytes)
+		}
+	}
+}

From 08795eaaf5924754ba60ee9bd8ada9eee187732c Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Mon, 8 Apr 2024 21:09:07 +0300
Subject: [PATCH 2/8] spaces

---
 .../AsyncAwait/HTTPClientResponse.swift        | 18 +++++++++---------
 .../AsyncHTTPClient/FoundationExtensions.swift | 14 +++++++-------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
index 4476c90c4..8ae819a88 100644
--- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
+++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
@@ -140,15 +140,15 @@ extension HTTPClientResponse {
 
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 extension HTTPClientResponse {
-	/// Response body as `ByteBuffer`.
-	public var bytes: ByteBuffer {
-		get async throws {
-			let expectedBytes = headers
-				.first(name: "content-length")
-				.flatMap(Int.init) ?? 1024 * 1024
-			return try await body.collect(upTo: expectedBytes)
-		}
-	}
+    /// Response body as `ByteBuffer`.
+    public var bytes: ByteBuffer {
+        get async throws {
+            let expectedBytes = headers
+                .first(name: "content-length")
+                .flatMap(Int.init) ?? 1024 * 1024
+            return try await body.collect(upTo: expectedBytes)
+        }
+    }
 }
 
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift
index ca27f2459..f5c747e04 100644
--- a/Sources/AsyncHTTPClient/FoundationExtensions.swift
+++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift
@@ -67,11 +67,11 @@ extension HTTPClient.Body {
 
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 extension HTTPClientResponse {
-	/// Response body as `Data`.
-	public var data: Data? {
-		get async throws {
-			var bytes = try await bytes
-			return bytes.readData(length: bytes.readableBytes)
-		}
-	}
+    /// Response body as `Data`.
+    public var data: Data? {
+        get async throws {
+            var bytes = try await bytes
+            return bytes.readData(length: bytes.readableBytes)
+        }
+    }
 }

From 8f1e2eaf38183816b5d51ebf08012d1641a081cf Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 9 Apr 2024 12:07:32 +0300
Subject: [PATCH 3/8] use self.bytes

---
 Sources/AsyncHTTPClient/FoundationExtensions.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift
index f5c747e04..95b8cf905 100644
--- a/Sources/AsyncHTTPClient/FoundationExtensions.swift
+++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift
@@ -70,7 +70,7 @@ extension HTTPClientResponse {
     /// Response body as `Data`.
     public var data: Data? {
         get async throws {
-            var bytes = try await bytes
+            var bytes = try await self.bytes
             return bytes.readData(length: bytes.readableBytes)
         }
     }

From 7e0ebe21d01b1f5f9e15999e2d39bb0cbd1f211b Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 9 Apr 2024 12:11:23 +0300
Subject: [PATCH 4/8] changed bytes from a computed property to a func. Removed
 1Mb expected bytes

---
 .../AsyncAwait/HTTPClientResponse.swift         | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
index 8ae819a88..a46e2b4d8 100644
--- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
+++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
@@ -141,13 +141,18 @@ extension HTTPClientResponse {
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 extension HTTPClientResponse {
     /// Response body as `ByteBuffer`.
-    public var bytes: ByteBuffer {
-        get async throws {
-            let expectedBytes = headers
-                .first(name: "content-length")
-                .flatMap(Int.init) ?? 1024 * 1024
-            return try await body.collect(upTo: expectedBytes)
+    /// - Returns: Bytes collected over time
+    public func bytes() async throws -> ByteBuffer {
+        if let expectedBytes = self.headers.first(name: "content-length").flatMap(Int.init) {
+            return try await self.body.collect(upTo: expectedBytes)
         }
+
+        var data = [UInt8]()
+        for try await var buffer in self.body {
+            data = data + (buffer.readBytes(length: buffer.readableBytes) ?? [])
+        }
+
+        return ByteBuffer(bytes: data)
     }
 }
 

From 47930100caee93769b81d613f9f681faafa5f3f7 Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 9 Apr 2024 12:11:38 +0300
Subject: [PATCH 5/8] use bytes()

---
 Sources/AsyncHTTPClient/FoundationExtensions.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift
index 95b8cf905..68de0723e 100644
--- a/Sources/AsyncHTTPClient/FoundationExtensions.swift
+++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift
@@ -70,7 +70,7 @@ extension HTTPClientResponse {
     /// Response body as `Data`.
     public var data: Data? {
         get async throws {
-            var bytes = try await self.bytes
+            var bytes = try await self.bytes()
             return bytes.readData(length: bytes.readableBytes)
         }
     }

From 7debf11242af72f02d9c4031d89cd026100f46ed Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 9 Apr 2024 12:36:07 +0300
Subject: [PATCH 6/8] added tests for new helpers

---
 .../AsyncAwaitEndToEndTests.swift             | 44 +++++++++++++++++++
 1 file changed, 44 insertions(+)

diff --git a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
index a30a8cf91..bd62f7be5 100644
--- a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
+++ b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
@@ -835,6 +835,50 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
             }
         }
     }
+
+    func testResponseBytesHelper() {
+        XCTAsyncTest {
+            let bin = HTTPBin(.http2(compress: false)) { _ in HTTPEchoHandler() }
+            defer { XCTAssertNoThrow(try bin.shutdown()) }
+            let client = makeDefaultHTTPClient()
+            defer { XCTAssertNoThrow(try client.syncShutdown()) }
+            let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
+            var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/")
+            request.method = .POST
+            request.body = .bytes(ByteBuffer(string: "1234"))
+
+            guard let response = await XCTAssertNoThrowWithResult(
+                try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
+            ) else { return }
+            XCTAssertEqual(response.headers["content-length"], ["4"])
+            guard let body = await XCTAssertNoThrowWithResult(
+                try await response.bytes()
+            ) else { return }
+            XCTAssertEqual(body, ByteBuffer(string: "1234"))
+        }
+    }
+
+    func testResponseBodyDataHelper() {
+        XCTAsyncTest {
+            let bin = HTTPBin(.http2(compress: false)) { _ in HTTPEchoHandler() }
+            defer { XCTAssertNoThrow(try bin.shutdown()) }
+            let client = makeDefaultHTTPClient()
+            defer { XCTAssertNoThrow(try client.syncShutdown()) }
+            let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
+            var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/")
+            request.method = .POST
+            request.body = .bytes(ByteBuffer(string: "1234"))
+
+            guard let response = await XCTAssertNoThrowWithResult(
+                try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
+            ) else { return }
+            XCTAssertEqual(response.headers["content-length"], ["4"])
+            guard let bodyData = await XCTAssertNoThrowWithResult(
+                try await response.data
+            ) else { return }
+            XCTAssertEqual(bodyData, "1234".data(using: .utf8))
+        }
+    }
 }
 
 struct AnySendableSequence<Element>: @unchecked Sendable {

From 06d365fa6ce79433504fb10068ce3ec0f60aff71 Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 9 Apr 2024 12:54:03 +0300
Subject: [PATCH 7/8] maxBytes param for bytes() func. tests updated

---
 .../AsyncAwait/HTTPClientResponse.swift               |  6 ++++--
 .../AsyncAwaitEndToEndTests.swift                     | 11 ++++++++++-
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
index a46e2b4d8..c598aefa9 100644
--- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
+++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
@@ -141,9 +141,11 @@ extension HTTPClientResponse {
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 extension HTTPClientResponse {
     /// Response body as `ByteBuffer`.
+    /// - Parameter maxBytes: The maximum number of bytes this method is allowed to accumulate. 
+    /// Will accumulate all available bytes if nil passed.
     /// - Returns: Bytes collected over time
-    public func bytes() async throws -> ByteBuffer {
-        if let expectedBytes = self.headers.first(name: "content-length").flatMap(Int.init) {
+    public func bytes(upTo maxBytes: Int? = nil) async throws -> ByteBuffer {
+        if let expectedBytes = maxBytes ?? self.headers.first(name: "content-length").flatMap(Int.init) {
             return try await self.body.collect(upTo: expectedBytes)
         }
 
diff --git a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
index bd62f7be5..be1b3f43e 100644
--- a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
+++ b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
@@ -852,9 +852,18 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
             ) else { return }
             XCTAssertEqual(response.headers["content-length"], ["4"])
             guard let body = await XCTAssertNoThrowWithResult(
-                try await response.bytes()
+                try await response.bytes(upTo: 4)
             ) else { return }
             XCTAssertEqual(body, ByteBuffer(string: "1234"))
+
+            guard var responseNoContentLength = await XCTAssertNoThrowWithResult(
+                try await client.execute(request, deadline: .now() + .seconds(10), logger: logger)
+            ) else { return }
+            responseNoContentLength.headers.remove(name: "content-length")
+            guard let body2 = await XCTAssertNoThrowWithResult(
+                try await responseNoContentLength.bytes()
+            ) else { return }
+            XCTAssertEqual(body2, ByteBuffer(string: "1234"))
         }
     }
 

From d76e399cea051ba3d633f9468a33d69af4821969 Mon Sep 17 00:00:00 2001
From: Sergey Armodin <dolph-in@yandex.ru>
Date: Tue, 16 Apr 2024 00:44:01 +0300
Subject: [PATCH 8/8] Made upTo a required parameter for bytes and data
 extensions

---
 .../AsyncAwait/HTTPClientResponse.swift           | 15 +++------------
 .../AsyncHTTPClient/FoundationExtensions.swift    | 10 +++++-----
 .../AsyncAwaitEndToEndTests.swift                 |  6 +++---
 3 files changed, 11 insertions(+), 20 deletions(-)

diff --git a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
index c598aefa9..24b194b10 100644
--- a/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
+++ b/Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift
@@ -142,19 +142,10 @@ extension HTTPClientResponse {
 extension HTTPClientResponse {
     /// Response body as `ByteBuffer`.
     /// - Parameter maxBytes: The maximum number of bytes this method is allowed to accumulate. 
-    /// Will accumulate all available bytes if nil passed.
     /// - Returns: Bytes collected over time
-    public func bytes(upTo maxBytes: Int? = nil) async throws -> ByteBuffer {
-        if let expectedBytes = maxBytes ?? self.headers.first(name: "content-length").flatMap(Int.init) {
-            return try await self.body.collect(upTo: expectedBytes)
-        }
-
-        var data = [UInt8]()
-        for try await var buffer in self.body {
-            data = data + (buffer.readBytes(length: buffer.readableBytes) ?? [])
-        }
-
-        return ByteBuffer(bytes: data)
+    public func bytes(upTo maxBytes: Int) async throws -> ByteBuffer {
+        let expectedBytes = self.headers.first(name: "content-length").flatMap(Int.init) ?? maxBytes
+        return try await self.body.collect(upTo: expectedBytes)
     }
 }
 
diff --git a/Sources/AsyncHTTPClient/FoundationExtensions.swift b/Sources/AsyncHTTPClient/FoundationExtensions.swift
index 68de0723e..42b95b6d8 100644
--- a/Sources/AsyncHTTPClient/FoundationExtensions.swift
+++ b/Sources/AsyncHTTPClient/FoundationExtensions.swift
@@ -68,10 +68,10 @@ extension HTTPClient.Body {
 @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
 extension HTTPClientResponse {
     /// Response body as `Data`.
-    public var data: Data? {
-        get async throws {
-            var bytes = try await self.bytes()
-            return bytes.readData(length: bytes.readableBytes)
-        }
+    /// - Parameter maxBytes: The maximum number of bytes this method is allowed to accumulate.
+    /// - Returns: Bytes collected over time
+    public func data(upTo maxBytes: Int) async throws -> Data? {
+        var bytes = try await self.bytes(upTo: maxBytes)
+        return bytes.readData(length: bytes.readableBytes)
     }
 }
diff --git a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
index be1b3f43e..bfce896b6 100644
--- a/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
+++ b/Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift
@@ -852,7 +852,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
             ) else { return }
             XCTAssertEqual(response.headers["content-length"], ["4"])
             guard let body = await XCTAssertNoThrowWithResult(
-                try await response.bytes(upTo: 4)
+                try await response.bytes(upTo: 3)
             ) else { return }
             XCTAssertEqual(body, ByteBuffer(string: "1234"))
 
@@ -861,7 +861,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
             ) else { return }
             responseNoContentLength.headers.remove(name: "content-length")
             guard let body2 = await XCTAssertNoThrowWithResult(
-                try await responseNoContentLength.bytes()
+                try await responseNoContentLength.bytes(upTo: 4)
             ) else { return }
             XCTAssertEqual(body2, ByteBuffer(string: "1234"))
         }
@@ -883,7 +883,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
             ) else { return }
             XCTAssertEqual(response.headers["content-length"], ["4"])
             guard let bodyData = await XCTAssertNoThrowWithResult(
-                try await response.data
+                try await response.data(upTo: 4)
             ) else { return }
             XCTAssertEqual(bodyData, "1234".data(using: .utf8))
         }