From ad4f7eaec53db3e2536e9b820b9d49fe871362a0 Mon Sep 17 00:00:00 2001 From: Sergey Armodin 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 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 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 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 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 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: @unchecked Sendable { From 06d365fa6ce79433504fb10068ce3ec0f60aff71 Mon Sep 17 00:00:00 2001 From: Sergey Armodin 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 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)) }