From e1b2a99d125fa5431b1e22d7e3c6139360d0cca8 Mon Sep 17 00:00:00 2001 From: Chris McGee <87777443+cmcgee1024@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:37:39 -0400 Subject: [PATCH] Provide documentation and context information for NIOTooManyBytesError (#2831) ### Motivation: The NIOTooManyBytesError doesn't have any documentation for someone that encounters this error. Also, they have no idea the magnitude of the limit that was set to decide if the payload is an unreasonable size. ### Modifications: Provide documentation that explains the situation when the error occurs, which is the upTo limit of an AsyncSequence is exceeded. Describe one potential action, which is to increase this limit. Provide the maxBytes in the error so that the user can gauge whether the upTo limit is already reasonable and the payload size is excessive, or the limit needs to be increased to suit more situations. ### Result: There will be documentation for the NIOTooManyBytesError so that if someone looks it up they will have a better understanding of the situation, and possible actions. The limit will be included in the error to give them an idea of the scale of the payload limit that is in place, or potentially the configuration value that they can change. --------- Co-authored-by: Cory Benfield --- Sources/NIOCore/AsyncAwaitSupport.swift | 45 ++++++++++++++++++--- Tests/NIOCoreTests/AsyncSequenceTests.swift | 35 +++++++++++++++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/Sources/NIOCore/AsyncAwaitSupport.swift b/Sources/NIOCore/AsyncAwaitSupport.swift index abdc20c2d6..05e8061716 100644 --- a/Sources/NIOCore/AsyncAwaitSupport.swift +++ b/Sources/NIOCore/AsyncAwaitSupport.swift @@ -240,8 +240,43 @@ extension ChannelPipeline { } } -public struct NIOTooManyBytesError: Error, Hashable { - public init() {} +/// An error that is thrown when the number of bytes in an AsyncSequence exceeds the limit. +/// +/// When collecting the bytes from an AsyncSequence, there is a limit up to where the content +/// exceeds a certain threshold beyond which the content isn't matching an expected reasonable +/// size to be processed. This error is generally thrown when it is discovered that there are more +/// more bytes in a sequence than what was specified as the maximum. It could be that this upTo +/// limit should be increased, or that the sequence has unexpected content in it. +public struct NIOTooManyBytesError: Error { + /// Current limit on the maximum number of bytes in the sequence + public var maxBytes: Int? + + @available( + *, + deprecated, + message: "Construct the NIOTooManyBytesError with the maxBytes limit that triggered this error" + ) + public init() { + self.maxBytes = nil + } + + public init(maxBytes: Int) { + self.maxBytes = maxBytes + } +} + +extension NIOTooManyBytesError: Equatable { + public static func == (lhs: NIOTooManyBytesError, rhs: NIOTooManyBytesError) -> Bool { + // Equality of the maxBytes isn't of consequence + true + } +} + +extension NIOTooManyBytesError: Hashable { + public func hash(into hasher: inout Hasher) { + // All errors of this type hash to the same value since maxBytes isn't of consequence + hasher.combine(7) + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -262,7 +297,7 @@ extension AsyncSequence where Element: RandomAccessCollection, Element.Element = for try await fragment in self { bytesRead += fragment.count guard bytesRead <= maxBytes else { - throw NIOTooManyBytesError() + throw NIOTooManyBytesError(maxBytes: maxBytes) } accumulationBuffer.writeBytes(fragment) } @@ -305,7 +340,7 @@ extension AsyncSequence where Element == ByteBuffer { for try await fragment in self { bytesRead += fragment.readableBytes guard bytesRead <= maxBytes else { - throw NIOTooManyBytesError() + throw NIOTooManyBytesError(maxBytes: maxBytes) } accumulationBuffer.writeImmutableBuffer(fragment) } @@ -328,7 +363,7 @@ extension AsyncSequence where Element == ByteBuffer { return ByteBuffer() } guard head.readableBytes <= maxBytes else { - throw NIOTooManyBytesError() + throw NIOTooManyBytesError(maxBytes: maxBytes) } let tail = AsyncSequenceFromIterator(iterator) diff --git a/Tests/NIOCoreTests/AsyncSequenceTests.swift b/Tests/NIOCoreTests/AsyncSequenceTests.swift index 069519b40f..7168605868 100644 --- a/Tests/NIOCoreTests/AsyncSequenceTests.swift +++ b/Tests/NIOCoreTests/AsyncSequenceTests.swift @@ -126,10 +126,11 @@ final class AsyncSequenceCollectTests: XCTestCase { } // test for the generic version + let maxBytes = max(expectedBytes.count - 1, 0) await XCTAssertThrowsError( try await testCase.buffers .asAsyncSequence() - .collect(upTo: max(expectedBytes.count - 1, 0), using: .init()), + .collect(upTo: maxBytes, using: .init()), file: testCase.file, line: testCase.line ) { error in @@ -138,6 +139,21 @@ final class AsyncSequenceCollectTests: XCTestCase { file: testCase.file, line: testCase.line ) + guard let tooManyBytesErr = error as? NIOTooManyBytesError else { + XCTFail( + "Error was not an NIOTooManyBytesError", + file: testCase.file, + line: testCase.line + ) + return + } + + XCTAssertEqual( + maxBytes, + tooManyBytesErr.maxBytes, + file: testCase.file, + line: testCase.line + ) } // test for the `ByteBuffer` optimised version @@ -145,7 +161,7 @@ final class AsyncSequenceCollectTests: XCTestCase { try await testCase.buffers .map(ByteBuffer.init(bytes:)) .asAsyncSequence() - .collect(upTo: max(expectedBytes.count - 1, 0)), + .collect(upTo: maxBytes), file: testCase.file, line: testCase.line ) { error in @@ -154,6 +170,21 @@ final class AsyncSequenceCollectTests: XCTestCase { file: testCase.file, line: testCase.line ) + guard let tooManyBytesErr = error as? NIOTooManyBytesError else { + XCTFail( + "Error was not an NIOTooManyBytesError", + file: testCase.file, + line: testCase.line + ) + return + } + + // Sometimes the max bytes is subtracted from the header size + XCTAssertTrue( + tooManyBytesErr.maxBytes != nil && tooManyBytesErr.maxBytes! <= maxBytes, + file: testCase.file, + line: testCase.line + ) } } }