diff --git a/Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift b/Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift index 6e24c0b..4514c9d 100644 --- a/Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift +++ b/Sources/OpenAPIURLSession/URLSessionBidirectionalStreaming/HTTPBodyOutputStreamBridge.swift @@ -63,6 +63,9 @@ final class HTTPBodyOutputStreamBridge: NSObject, StreamDelegate { let task = Task { dispatchPrecondition(condition: .notOnQueue(Self.streamQueue)) for try await chunk in httpBody { + // Empty chunks are legal HTTPBody values (for example text/plain "") + // but OutputStream cannot be written with a zero length buffer. + guard !chunk.isEmpty else { continue } try await withCheckedThrowingContinuation { continuation in Self.streamQueue.async { debug("Output stream delegate produced chunk and suspended producer.") diff --git a/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/HTTPBodyOutputStreamTests.swift b/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/HTTPBodyOutputStreamTests.swift index a2d5263..761d5f0 100644 --- a/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/HTTPBodyOutputStreamTests.swift +++ b/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/HTTPBodyOutputStreamTests.swift @@ -72,6 +72,35 @@ class HTTPBodyOutputStreamBridgeTests: XCTestCase { ) } + func testHTTPBodyOutputStreamEmptyChunkDoesNotCrash() async throws { + let emptyChunks: [[UInt8]] = [[]] + let requestByteSequence = MockAsyncSequence(elementsToVend: emptyChunks, gatingProduction: false) + let requestBody = HTTPBody(requestByteSequence, length: .known(0), iterationBehavior: .single) + + var inputStream: InputStream? + var outputStream: OutputStream? + Stream.getBoundStreams(withBufferSize: 16, inputStream: &inputStream, outputStream: &outputStream) + guard let inputStream, let outputStream else { fatalError("getBoundStreams did not return non-nil streams") } + + let requestStream = HTTPBodyOutputStreamBridge(outputStream, requestBody) + let delegate = MockInputStreamDelegate(inputStream: inputStream) + + var data = [UInt8]() + while let inputStreamBytes = try await delegate.waitForBytes(maxBytes: 4096) { + data.append(contentsOf: inputStreamBytes) + } + XCTAssertEqual(data, []) + XCTAssertEqual(inputStream.streamStatus, .closed) + XCTAssertNil(inputStream.streamError) + + HTTPBodyOutputStreamBridge.streamQueue.asyncAndWait( + execute: DispatchWorkItem { + XCTAssertEqual(requestStream.outputStream.streamStatus, .closed) + XCTAssertNil(requestStream.outputStream.streamError) + } + ) + } + func testHTTPBodyOutputStreamBridgeBackpressure() async throws { let chunkSize = 71 let streamBufferSize = 37