diff --git a/Package.swift b/Package.swift index a781bbcb8..de4a218fc 100644 --- a/Package.swift +++ b/Package.swift @@ -330,5 +330,9 @@ let package = Package( name: "SmithyHTTPAPITests", dependencies: ["SmithyHTTPAPI"] ), + .testTarget( + name: "SmithyStreamsTests", + dependencies: ["SmithyStreams", "Smithy"] + ), ].compactMap { $0 } ) diff --git a/Sources/Smithy/Stream.swift b/Sources/Smithy/Stream.swift index 43a351812..6ab719578 100644 --- a/Sources/Smithy/Stream.swift +++ b/Sources/Smithy/Stream.swift @@ -64,6 +64,7 @@ public enum StreamError: Error, Sendable { case invalidOffset(String) case notSupported(String) case connectionReleased(String) + case writeToClosedStream(String) } extension Stream { diff --git a/Sources/SmithyStreams/BufferedStream.swift b/Sources/SmithyStreams/BufferedStream.swift index d720597f2..702ea2bc7 100644 --- a/Sources/SmithyStreams/BufferedStream.swift +++ b/Sources/SmithyStreams/BufferedStream.swift @@ -8,6 +8,7 @@ import struct Foundation.Data import class Foundation.NSRecursiveLock import protocol Smithy.Stream +import enum Smithy.StreamError /// A `Stream` implementation that buffers data in memory. /// The buffer size depends on the amount of data written and read. @@ -207,8 +208,15 @@ public class BufferedStream: Stream, @unchecked Sendable { /// Writes the specified data to the stream. /// Then, continues a suspended reader (if any) to read the data. /// - Parameter data: The data to write. + /// - Throws: `StreamError.writeToClosedStream` if a write is attempted after the stream is closed. public func write(contentsOf data: Data) throws { - lock.withLockingClosure { + try lock.withLockingClosure { + // Do not allow writing to stream once it closes. + // This ensures length of stream does not change once it reports a length. + // Throw `StreamError.writeToClosedStream` to alert the caller. + guard !isClosed else { + throw StreamError.writeToClosedStream("Attempt to write to closed stream") + } // append the data to the buffer // this will increase the in-memory size of the buffer _buffer.append(data) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift b/Tests/SmithyStreamsTests/BufferedStreamTests.swift similarity index 89% rename from Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift rename to Tests/SmithyStreamsTests/BufferedStreamTests.swift index 052e13ef0..2c08699a5 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift +++ b/Tests/SmithyStreamsTests/BufferedStreamTests.swift @@ -6,7 +6,7 @@ // import XCTest -import ClientRuntime +import enum Smithy.StreamError import class SmithyStreams.BufferedStream final class BufferedStreamTests: XCTestCase { @@ -208,6 +208,29 @@ final class BufferedStreamTests: XCTestCase { XCTAssertEqual(testData + additionalData, readData1) } + func test_write_throwsWriteToClosedStreamErrorWhenStreamIsClosed() throws { + let subject = BufferedStream(data: testData, isClosed: true) + XCTAssertThrowsError( + try subject.write(contentsOf: additionalData), + "No error was thrown when writing to a closed stream", + { error in + switch error { + case StreamError.writeToClosedStream: + break // expected error, test passes + default: + XCTFail("Error was thrown, but not of expected error type") + } + } + ) + } + + func test_write_rejectsAdditionalDataWhenStreamIsClosed() throws { + let subject = BufferedStream(data: testData, isClosed: true) + XCTAssertThrowsError(try subject.write(contentsOf: additionalData)) + let readData = try subject.read(upToCount: Int.max) + XCTAssertEqual(testData, readData) + } + // MARK: - length func test_length_returnsNilLengthBeforeStreamCloses() throws {