Skip to content

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

Merged
merged 18 commits into from
Apr 4, 2023
Merged

Conversation

carolinacass
Copy link
Contributor

Motivation:
not accumulate too many bytes

Modifications:
Implementing collect function to use NIOCore version to prevent overflowing

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)
Copy link
Collaborator

@dnadoba dnadoba Mar 16, 2023

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.

Copy link
Contributor Author

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.

@@ -83,6 +83,15 @@ extension HTTPClientResponse {
@inlinable public func makeAsyncIterator() -> AsyncIterator {
.init(storage: self.storage.makeAsyncIterator())
}

/// Accumulates an ``Swift/AsyncSequence`` of ``ByteBuffer``s into a single ``ByteBuffer``.
Copy link
Collaborator

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.

Copy link
Collaborator

@dnadoba dnadoba left a 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.

@@ -32,16 +32,32 @@ public struct HTTPClientResponse: Sendable {
/// The body of this HTTP response.
public var body: Body

@usableFromInline internal var requestMethod: HTTPMethod?
Copy link
Collaborator

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.

Comment on lines 109 to 110
print(maxBytes)
print("contentLength", contentLength)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment on lines 139 to 140
let contentLengths = headers["content-length"].first.flatMap({Int($0, radix: 10)})
return contentLengths
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

@@ -146,7 +201,7 @@ extension HTTPClientResponse.Body {
@inlinable public static func stream<SequenceOfBytes>(
_ sequenceOfBytes: SequenceOfBytes
) -> Self where SequenceOfBytes: AsyncSequence & Sendable, SequenceOfBytes.Element == ByteBuffer {
self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)))
self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)), expectedContentLength: nil)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)), expectedContentLength: nil)
self.init(.anyAsyncSequence(AnyAsyncSequence(sequenceOfBytes.singleIteratorPrecondition)))

Comment on lines 54 to 64
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)
}
}
Copy link
Collaborator

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still here

Comment on lines 23 to 35
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:))
)
}
Copy link
Collaborator

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still here

Comment on lines 763 to 764
response.headers.replaceOrAdd(name: "content-length", value: "1025")
XCTAssertEqual(response.headers["content-length"], ["1025"])
Copy link
Collaborator

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"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
request.body = .bytes(ByteBuffer(string: "1235"))
request.body = .bytes(ByteBuffer(string: "1234"))

Comment on lines 142 to 143
}
else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
else {
} else {

Comment on lines 23 to 35
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:))
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still here

Comment on lines 54 to 64
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)
}
}
Copy link
Collaborator

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?
Copy link
Collaborator

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(
Copy link
Collaborator

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

@dnadoba dnadoba requested a review from Lukasa March 23, 2023 11:25
@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Mar 24, 2023
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"))
Copy link
Collaborator

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.

@carolinacass carolinacass requested a review from Lukasa March 29, 2023 15:33
@Lukasa
Copy link
Collaborator

Lukasa commented Mar 30, 2023

@swift-server-bot add to allowlist

Copy link
Collaborator

@Lukasa Lukasa left a 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.

@Lukasa
Copy link
Collaborator

Lukasa commented Mar 30, 2023

Ah, sorry, can you run generate_linux_tests.rb please?

@Lukasa
Copy link
Collaborator

Lukasa commented Mar 30, 2023

You'll also need to run ./scripts/soundness.sh.

@Lukasa Lukasa merged commit 91b2640 into swift-server:main Apr 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants