Skip to content

Commit ebc8fe1

Browse files
committed
Handle ResponseAccumulator not being able to buffer large response in memory
Check content length header for early exit
1 parent f7a84af commit ebc8fe1

File tree

3 files changed

+134
-1
lines changed

3 files changed

+134
-1
lines changed

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ extension HTTPClient {
356356
///
357357
/// This ``HTTPClientResponseDelegate`` buffers a complete HTTP response in memory. It does not stream the response body in.
358358
/// The resulting ``Response`` type is ``HTTPClient/Response``.
359-
public class ResponseAccumulator: HTTPClientResponseDelegate {
359+
final public class ResponseAccumulator: HTTPClientResponseDelegate {
360360
public typealias Response = HTTPClient.Response
361361

362362
enum State {
@@ -366,9 +366,33 @@ public class ResponseAccumulator: HTTPClientResponseDelegate {
366366
case end
367367
case error(Error)
368368
}
369+
370+
public struct ResponseTooBigError: Error, CustomStringConvertible {
371+
var maxBodySize: Int
372+
373+
public var description: String {
374+
return "ResponseTooBigError: received response body exceeds maximum accepted size of \(maxBodySize) bytes"
375+
}
376+
}
369377

370378
var state = State.idle
371379
let request: HTTPClient.Request
380+
381+
static let maxByteBufferSize = Int(UInt32.max)
382+
383+
/// Maximum size in bytes of the HTTP response body that ``ResponseAccumulator`` will accept
384+
/// until it will abort the request and throw an ``ResponseTooBigError``.
385+
///
386+
/// Default is 2^32.
387+
/// - precondition: not allowed to exceed 2^32 because ``ByteBuffer`` can not store more bytes
388+
public var maxBodySize: Int = maxByteBufferSize {
389+
didSet {
390+
precondition(
391+
maxBodySize <= Self.maxByteBufferSize,
392+
"maxBodyLength is not allowed to exceed 2^32 because ByteBuffer can not store more bytes"
393+
)
394+
}
395+
}
372396

373397
public init(request: HTTPClient.Request) {
374398
self.request = request
@@ -377,6 +401,14 @@ public class ResponseAccumulator: HTTPClientResponseDelegate {
377401
public func didReceiveHead(task: HTTPClient.Task<Response>, _ head: HTTPResponseHead) -> EventLoopFuture<Void> {
378402
switch self.state {
379403
case .idle:
404+
if let contentLength = head.headers.first(name: "Content-Length"),
405+
let announcedBodySize = Int(contentLength),
406+
announcedBodySize > self.maxBodySize {
407+
let error = ResponseTooBigError(maxBodySize: maxBodySize)
408+
self.state = .error(error)
409+
return task.eventLoop.makeFailedFuture(error)
410+
}
411+
380412
self.state = .head(head)
381413
case .head:
382414
preconditionFailure("head already set")
@@ -395,8 +427,20 @@ public class ResponseAccumulator: HTTPClientResponseDelegate {
395427
case .idle:
396428
preconditionFailure("no head received before body")
397429
case .head(let head):
430+
guard part.readableBytes <= self.maxBodySize else {
431+
let error = ResponseTooBigError(maxBodySize: self.maxBodySize)
432+
self.state = .error(error)
433+
return task.eventLoop.makeFailedFuture(error)
434+
}
398435
self.state = .body(head, part)
399436
case .body(let head, var body):
437+
let newBufferSize = body.writerIndex + part.readableBytes
438+
guard newBufferSize <= self.maxBodySize else {
439+
let error = ResponseTooBigError(maxBodySize: self.maxBodySize)
440+
self.state = .error(error)
441+
return task.eventLoop.makeFailedFuture(error)
442+
}
443+
400444
// The compiler can't prove that `self.state` is dead here (and it kinda isn't, there's
401445
// a cross-module call in the way) so we need to drop the original reference to `body` in
402446
// `self.state` or we'll get a CoW. To fix that we temporarily set the state to `.end` (which

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ internal final class HTTPBin<RequestHandler: ChannelInboundHandler> where
365365
var socketAddress: SocketAddress {
366366
return self.serverChannel.localAddress!
367367
}
368+
369+
370+
var baseURL: String {
371+
let scheme: String = {
372+
switch mode {
373+
case .http1_1, .refuse:
374+
return "http"
375+
case .http2:
376+
return "https"
377+
}
378+
}()
379+
return "\(scheme)://localhost:\(self.port)/"
380+
}
368381

369382
private let mode: Mode
370383
private let sslContext: NIOSSLContext?

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3091,6 +3091,82 @@ class HTTPClientTests: XCTestCase {
30913091
XCTAssertNoThrow(try future.wait())
30923092
XCTAssertNil(try delegate.next().wait())
30933093
}
3094+
3095+
func testResponseAccumulatorMaxBodySizeLimitExceedingWithContentLength() throws {
3096+
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in HTTPEchoHandler() }
3097+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
3098+
3099+
let body = ByteBuffer(bytes: 0..<11)
3100+
3101+
var request = try Request(url: httpBin.baseURL)
3102+
request.body = .byteBuffer(body)
3103+
let delegate = ResponseAccumulator(request: request)
3104+
delegate.maxBodySize = 10
3105+
XCTAssertThrowsError(try self.defaultClient.execute(
3106+
request: request,
3107+
delegate: delegate
3108+
).wait()) { error in
3109+
XCTAssertTrue(error is ResponseAccumulator.ResponseTooBigError, "unexpected error \(error)")
3110+
}
3111+
}
3112+
3113+
func testResponseAccumulatorMaxBodySizeLimitNotExceedingWithContentLength() throws {
3114+
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in HTTPEchoHandler() }
3115+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
3116+
3117+
let body = ByteBuffer(bytes: 0..<10)
3118+
3119+
var request = try Request(url: httpBin.baseURL)
3120+
request.body = .byteBuffer(body)
3121+
let delegate = ResponseAccumulator(request: request)
3122+
delegate.maxBodySize = 10
3123+
let response = try self.defaultClient.execute(
3124+
request: request,
3125+
delegate: delegate
3126+
).wait()
3127+
3128+
XCTAssertEqual(response.body, body)
3129+
}
3130+
3131+
func testResponseAccumulatorMaxBodySizeLimitExceedingWithTransferEncodingChuncked() throws {
3132+
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in HTTPEchoHandler() }
3133+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
3134+
3135+
let body = ByteBuffer(bytes: 0..<11)
3136+
3137+
var request = try Request(url: httpBin.baseURL)
3138+
request.body = .stream { writer in
3139+
writer.write(.byteBuffer(body))
3140+
}
3141+
let delegate = ResponseAccumulator(request: request)
3142+
delegate.maxBodySize = 10
3143+
XCTAssertThrowsError(try self.defaultClient.execute(
3144+
request: request,
3145+
delegate: delegate
3146+
).wait()) { error in
3147+
XCTAssertTrue(error is ResponseAccumulator.ResponseTooBigError, "unexpected error \(error)")
3148+
}
3149+
}
3150+
3151+
func testResponseAccumulatorMaxBodySizeLimitNotExceedingWithTransferEncodingChuncked() throws {
3152+
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in HTTPEchoHandler() }
3153+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
3154+
3155+
let body = ByteBuffer(bytes: 0..<10)
3156+
3157+
var request = try Request(url: httpBin.baseURL)
3158+
request.body = .stream { writer in
3159+
writer.write(.byteBuffer(body))
3160+
}
3161+
let delegate = ResponseAccumulator(request: request)
3162+
delegate.maxBodySize = 10
3163+
let response = try self.defaultClient.execute(
3164+
request: request,
3165+
delegate: delegate
3166+
).wait()
3167+
3168+
XCTAssertEqual(response.body, body)
3169+
}
30943170

30953171
// In this test, we test that a request can continue to stream its body after the response head and end
30963172
// was received where the end is a 200.

0 commit comments

Comments
 (0)