-
Notifications
You must be signed in to change notification settings - Fork 125
Handle ResponseAccumulator not being able to buffer large response in memory #637
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
Changes from 7 commits
ebc8fe1
0b95c8a
918db8f
d4d61f7
a9122e8
f0947e9
3d8cac8
986bb05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -356,7 +356,7 @@ extension HTTPClient { | |
/// | ||
/// This ``HTTPClientResponseDelegate`` buffers a complete HTTP response in memory. It does not stream the response body in. | ||
/// The resulting ``Response`` type is ``HTTPClient/Response``. | ||
public class ResponseAccumulator: HTTPClientResponseDelegate { | ||
public final class ResponseAccumulator: HTTPClientResponseDelegate { | ||
public typealias Response = HTTPClient.Response | ||
|
||
enum State { | ||
|
@@ -367,16 +367,49 @@ public class ResponseAccumulator: HTTPClientResponseDelegate { | |
case error(Error) | ||
} | ||
|
||
public struct ResponseTooBigError: Error, CustomStringConvertible { | ||
var maxBodySize: Int | ||
|
||
public var description: String { | ||
return "ResponseTooBigError: received response body exceeds maximum accepted size of \(self.maxBodySize) bytes" | ||
} | ||
} | ||
|
||
var state = State.idle | ||
let request: HTTPClient.Request | ||
|
||
static let maxByteBufferSize = Int(UInt32.max) | ||
|
||
/// Maximum size in bytes of the HTTP response body that ``ResponseAccumulator`` will accept | ||
/// until it will abort the request and throw an ``ResponseTooBigError``. | ||
/// | ||
/// Default is 2^32. | ||
/// - precondition: not allowed to exceed 2^32 because ``ByteBuffer`` can not store more bytes | ||
public var maxBodySize: Int = maxByteBufferSize { | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
didSet { | ||
precondition(self.maxBodySize >= 0, "maxBodyLength is not allowed to be negative") | ||
precondition( | ||
self.maxBodySize <= Self.maxByteBufferSize, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relatively minor, but wouldn't it also make sense to verify that the new value isn't negative? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in f0947e9 |
||
"maxBodyLength is not allowed to exceed 2^32 because ByteBuffer can not store more bytes" | ||
) | ||
} | ||
} | ||
|
||
public init(request: HTTPClient.Request) { | ||
self.request = request | ||
} | ||
|
||
public func didReceiveHead(task: HTTPClient.Task<Response>, _ head: HTTPResponseHead) -> EventLoopFuture<Void> { | ||
switch self.state { | ||
case .idle: | ||
if let contentLength = head.headers.first(name: "Content-Length"), | ||
let announcedBodySize = Int(contentLength), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be tolerating the possibility of a non-integer Content-Length field here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's at least what we do in swift-nio so far: |
||
announcedBodySize > self.maxBodySize { | ||
let error = ResponseTooBigError(maxBodySize: maxBodySize) | ||
dnadoba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.state = .error(error) | ||
return task.eventLoop.makeFailedFuture(error) | ||
} | ||
|
||
self.state = .head(head) | ||
case .head: | ||
preconditionFailure("head already set") | ||
|
@@ -395,8 +428,20 @@ public class ResponseAccumulator: HTTPClientResponseDelegate { | |
case .idle: | ||
preconditionFailure("no head received before body") | ||
case .head(let head): | ||
guard part.readableBytes <= self.maxBodySize else { | ||
let error = ResponseTooBigError(maxBodySize: self.maxBodySize) | ||
self.state = .error(error) | ||
return task.eventLoop.makeFailedFuture(error) | ||
} | ||
self.state = .body(head, part) | ||
case .body(let head, var body): | ||
let newBufferSize = body.writerIndex + part.readableBytes | ||
guard newBufferSize <= self.maxBodySize else { | ||
let error = ResponseTooBigError(maxBodySize: self.maxBodySize) | ||
self.state = .error(error) | ||
return task.eventLoop.makeFailedFuture(error) | ||
} | ||
|
||
// The compiler can't prove that `self.state` is dead here (and it kinda isn't, there's | ||
// a cross-module call in the way) so we need to drop the original reference to `body` in | ||
// `self.state` or we'll get a CoW. To fix that we temporarily set the state to `.end` (which | ||
|
Uh oh!
There was an error while loading. Please reload this page.