Collect function fix#672
Conversation
Motivation: not accumulate too many bytes Modifications: Implementing collect function to use NIOCore version to prevent overflowing
| /// - Throws: `NIOTooManyBytesError` if the the sequence contains more than `maxBytes`. | ||
| /// - Returns: the number of bytes collected over time | ||
| func collect(maxBytes: Int) async throws -> ByteBuffer { | ||
| return try await self.collect(upTo: maxBytes) |
There was a problem hiding this comment.
If the response head already announces the body size we can check that size before downloading anything. This is e.g. the case if the response contains a Content-Length header which contains the expected body size. If it is bigger than maxBytes we should throw before calling self.collection(upTo:). To actually do that we need to move this extension from HTTPClientResponse.Body to HTTPClientResponse or have some kind of reference in HTTPClientResponse.Body to the headers.
Be careful though, there are edge cases where we don't expect a body even if Content-Length is set. This is e.g. the case if the request was just a HEAD request. I think there are more edge cases but that's the only one I know of the top of my head.
There was a problem hiding this comment.
I do not think this is appropriate for helper functions to check the Content-Length, but will confirm that it deals with it correctly.
| .init(storage: self.storage.makeAsyncIterator()) | ||
| } | ||
|
|
||
| /// Accumulates an ``Swift/AsyncSequence`` of ``ByteBuffer``s into a single ``ByteBuffer``. |
There was a problem hiding this comment.
We can be clearer here: this accumulates Body.
dnadoba
left a comment
There was a problem hiding this comment.
Great, a couple of small tweaks and we should be good to go.
| /// The body of this HTTP response. | ||
| public var body: Body | ||
|
|
||
| @usableFromInline internal var requestMethod: HTTPMethod? |
There was a problem hiding this comment.
we don't need the requestMethod any more as we compute the expextedContentLength in the init.
| print(maxBytes) | ||
| print("contentLength", contentLength) |
There was a problem hiding this comment.
| print(maxBytes) | |
| print("contentLength", contentLength) |
|
|
||
| @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
| extension HTTPClientResponse { | ||
| // need to pass in the arguments |
There was a problem hiding this comment.
| // need to pass in the arguments |
| @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
| extension HTTPClientResponse { | ||
| // need to pass in the arguments | ||
| static func expectedContentLength(requestMethod: HTTPMethod?, headers: HTTPHeaders, status: HTTPResponseStatus) -> Int? { |
There was a problem hiding this comment.
| static func expectedContentLength(requestMethod: HTTPMethod?, headers: HTTPHeaders, status: HTTPResponseStatus) -> Int? { | |
| static func expectedContentLength(requestMethod: HTTPMethod, headers: HTTPHeaders, status: HTTPResponseStatus) -> Int? { |
We make request method non-optional now
| let contentLengths = headers["content-length"].first.flatMap({Int($0, radix: 10)}) | ||
| return contentLengths |
There was a problem hiding this comment.
| let contentLengths = headers["content-length"].first.flatMap({Int($0, radix: 10)}) | |
| return contentLengths | |
| let contentLength = headers["content-length"].first.flatMap({Int($0, radix: 10)}) | |
| return contentLength |
| _ sequenceOfBytes: SequenceOfBytes | ||
| ) -> Self where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer { | ||
| self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition))) | ||
| self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)), expectedContentLength: nil) |
There was a problem hiding this comment.
| self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)), expectedContentLength: nil) | |
| self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition))) |
| func testReponseInitWithStatus() { | ||
| guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return } | ||
| XCTAsyncTest { | ||
| var response = HTTPClientResponse(status: .notModified , requestMethod: .GET) | ||
| response.headers.replaceOrAdd(name: "content-length", value: "1025") | ||
| guard let body = await XCTAssertNoThrowWithResult( | ||
| try await response.body.collect(upTo: 1024) | ||
| ) else { return } | ||
| XCTAssertEqual(0, body.readableBytes) | ||
| } | ||
| } |
There was a problem hiding this comment.
This is no longer testing what we want. I think we can just remove it
| private func makeDefaultHTTPClient( | ||
| eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .createNew | ||
| ) -> HTTPClient { | ||
| var config = HTTPClient.Configuration() | ||
| config.tlsConfiguration = .clientDefault | ||
| config.tlsConfiguration?.certificateVerification = .none | ||
| config.httpVersion = .automatic | ||
| return HTTPClient( | ||
| eventLoopGroupProvider: eventLoopGroupProvider, | ||
| configuration: config, | ||
| backgroundActivityLogger: Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:)) | ||
| ) | ||
| } |
There was a problem hiding this comment.
After we have removed the last test case, this is no longer used
| response.headers.replaceOrAdd(name: "content-length", value: "1025") | ||
| XCTAssertEqual(response.headers["content-length"], ["1025"]) |
There was a problem hiding this comment.
This is not affecting the test and we can remove it
| let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:)) | ||
| var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/") | ||
| request.method = .GET | ||
| request.body = .bytes(ByteBuffer(string: "1235")) |
There was a problem hiding this comment.
Nit:
| request.body = .bytes(ByteBuffer(string: "1235")) | |
| request.body = .bytes(ByteBuffer(string: "1234")) |
| } | ||
| else { |
There was a problem hiding this comment.
| } | |
| else { | |
| } else { |
| private func makeDefaultHTTPClient( | ||
| eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .createNew | ||
| ) -> HTTPClient { | ||
| var config = HTTPClient.Configuration() | ||
| config.tlsConfiguration = .clientDefault | ||
| config.tlsConfiguration?.certificateVerification = .none | ||
| config.httpVersion = .automatic | ||
| return HTTPClient( | ||
| eventLoopGroupProvider: eventLoopGroupProvider, | ||
| configuration: config, | ||
| backgroundActivityLogger: Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:)) | ||
| ) | ||
| } |
| func testReponseInitWithStatus() { | ||
| guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { return } | ||
| XCTAsyncTest { | ||
| var response = HTTPClientResponse(status: .notModified , requestMethod: .GET) | ||
| response.headers.replaceOrAdd(name: "content-length", value: "1025") | ||
| guard let body = await XCTAssertNoThrowWithResult( | ||
| try await response.body.collect(upTo: 1024) | ||
| ) else { return } | ||
| XCTAssertEqual(0, body.readableBytes) | ||
| } | ||
| } |
| status: HTTPResponseStatus = .ok, | ||
| headers: HTTPHeaders = [:], | ||
| body: Body = Body(), | ||
| requestMethod: HTTPMethod? |
There was a problem hiding this comment.
We can remove the requestMethod argument name again
| } | ||
|
|
||
| @inlinable public init( | ||
| @inlinable init( |
There was a problem hiding this comment.
This init needs to stay public
| let logger = Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:)) | ||
| var request = HTTPClientRequest(url: "https://localhost:\(bin.port)/") | ||
| request.method = .GET | ||
| request.body = .bytes(ByteBuffer(string: "1234")) |
There was a problem hiding this comment.
Hrm, I think this muddies the waters of the test somewhat. I don't think this test would fail if you took away the content-length checks, because the actual collect call will still end up violating the content-length.
What you want is for the HTTPbin server to send you back a content-length that's too long and no body at all. That should still throw this error.
|
@swift-server-bot add to allowlist |
|
Ah, sorry, can you run |
|
You'll also need to run |
Motivation:
not accumulate too many bytes
Modifications:
Implementing collect function to use NIOCore version to prevent overflowing