Skip to content

Commit 0dca77a

Browse files
committed
Add support for creating stream from byteBuffer, only allow one iterate of HBStreamedRequestBody
1 parent c569af7 commit 0dca77a

File tree

3 files changed

+46
-11
lines changed

3 files changed

+46
-11
lines changed

Sources/HummingbirdCore/Request/RequestBody.swift

+25-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import NIOConcurrencyHelpers
1516
import NIOCore
1617
import NIOHTTPTypes
1718

@@ -27,10 +28,11 @@ public enum HBRequestBody: Sendable, AsyncSequence {
2728

2829
public func makeAsyncIterator() -> HBStreamedRequestBody.AsyncIterator {
2930
switch self {
30-
case .byteBuffer:
31-
/// The server always creates the HBRequestBody as a stream. If it is converted
32-
/// into a single ByteBuffer it cannot be treated as a stream after that
33-
preconditionFailure("Cannot convert collapsed request body back into a sequence")
31+
case .byteBuffer(let buffer):
32+
let (stream, source) = NIOAsyncChannelInboundStream<HTTPRequestPart>.makeTestingStream()
33+
source.yield(.body(buffer))
34+
source.finish()
35+
return HBStreamedRequestBody(iterator: stream.makeAsyncIterator()).makeAsyncIterator()
3436
case .stream(let streamer):
3537
return streamer.makeAsyncIterator()
3638
}
@@ -49,13 +51,19 @@ public enum HBRequestBody: Sendable, AsyncSequence {
4951
}
5052

5153
/// Request body that is a stream of ByteBuffers.
52-
public struct HBStreamedRequestBody: Sendable, AsyncSequence {
54+
///
55+
/// This is a unicast async sequence that allows a single iterator to be created.
56+
public final class HBStreamedRequestBody: Sendable, AsyncSequence {
5357
public typealias Element = ByteBuffer
5458
public typealias InboundStream = NIOAsyncChannelInboundStream<HTTPRequestPart>
5559

60+
private let underlyingIterator: UnsafeTransfer<NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator>
61+
private let alreadyIterated: NIOLockedValueBox<Bool>
62+
5663
/// Initialize HBStreamedRequestBody from AsyncIterator of a NIOAsyncChannelInboundStream
5764
public init(iterator: InboundStream.AsyncIterator) {
5865
self.underlyingIterator = .init(iterator)
66+
self.alreadyIterated = .init(false)
5967
}
6068

6169
/// Async Iterator for HBStreamedRequestBody
@@ -65,9 +73,9 @@ public struct HBStreamedRequestBody: Sendable, AsyncSequence {
6573
private var underlyingIterator: InboundStream.AsyncIterator
6674
private var done: Bool
6775

68-
init(underlyingIterator: InboundStream.AsyncIterator) {
76+
init(underlyingIterator: InboundStream.AsyncIterator, done: Bool = false) {
6977
self.underlyingIterator = underlyingIterator
70-
self.done = false
78+
self.done = done
7179
}
7280

7381
public mutating func next() async throws -> ByteBuffer? {
@@ -88,8 +96,15 @@ public struct HBStreamedRequestBody: Sendable, AsyncSequence {
8896
}
8997

9098
public func makeAsyncIterator() -> AsyncIterator {
91-
AsyncIterator(underlyingIterator: self.underlyingIterator.wrappedValue)
99+
// verify if an iterator has already been created. If it has then create an
100+
// iterator that returns nothing. This could be a precondition failure (currently
101+
// an assert) as you should not be allowed to do this.
102+
let done = self.alreadyIterated.withLockedValue {
103+
assert($0 == false, "Can only create iterator from request body once")
104+
let done = $0
105+
$0 = true
106+
return done
107+
}
108+
return AsyncIterator(underlyingIterator: self.underlyingIterator.wrappedValue, done: done)
92109
}
93-
94-
private var underlyingIterator: UnsafeTransfer<NIOAsyncChannelInboundStream<HTTPRequestPart>.AsyncIterator>
95110
}

Sources/HummingbirdXCT/HBXCTRouter.swift

-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ struct HBXCTRouter<Responder: HBResponder>: HBXCTApplication where Responder.Con
112112
}
113113
let responseWriter = RouterResponseWriter()
114114
let trailerHeaders = try await response.body.write(responseWriter)
115-
for try await _ in request.body {}
116115
return responseWriter.collated.withLockedValue { collated in
117116
HBXCTResponse(head: response.head, body: collated, trailerHeaders: trailerHeaders)
118117
}

Tests/HummingbirdTests/ApplicationTests.swift

+21
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,27 @@ final class ApplicationTests: XCTestCase {
282282
}
283283
}
284284

285+
func testDoubleStreaming() async throws {
286+
let router = HBRouter()
287+
router.post("size") { request, context -> String in
288+
var request = request
289+
_ = try await request.collateBody(context: context)
290+
var size = 0
291+
for try await buffer in request.body {
292+
size += buffer.readableBytes
293+
}
294+
return size.description
295+
}
296+
let app = HBApplication(responder: router.buildResponder())
297+
298+
try await app.test(.router) { client in
299+
let buffer = self.randomBuffer(size: 100_000)
300+
try await client.XCTExecute(uri: "/size", method: .post, body: buffer) { response in
301+
XCTAssertEqual(String(buffer: response.body), "100000")
302+
}
303+
}
304+
}
305+
285306
func testOptional() async throws {
286307
let router = HBRouter()
287308
router

0 commit comments

Comments
 (0)