-
Notifications
You must be signed in to change notification settings - Fork 134
Collect function fix #672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can be clearer here: this accumulates Body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no longer testing what we want. I think we can just remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still here
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After we have removed the last test case, this is no longer used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still here
| response.headers.replaceOrAdd(name: "content-length", value: "1025") | ||
| XCTAssertEqual(response.headers["content-length"], ["1025"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
| request.body = .bytes(ByteBuffer(string: "1235")) | |
| request.body = .bytes(ByteBuffer(string: "1234")) |
| } | ||
| else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } | |
| 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:)) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still here
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still here
| status: HTTPResponseStatus = .ok, | ||
| headers: HTTPHeaders = [:], | ||
| body: Body = Body(), | ||
| requestMethod: HTTPMethod? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the requestMethod argument name again
| } | ||
|
|
||
| @inlinable public init( | ||
| @inlinable init( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one, this LGTM.
|
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