-
Notifications
You must be signed in to change notification settings - Fork 125
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.
@@ -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``. |
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.
@@ -32,16 +32,32 @@ public struct HTTPClientResponse: Sendable { | |||
/// 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 |
@@ -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) |
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