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
Default max body size for streaming request body collection #2312
Changes from 9 commits
18910f0
a86288f
f297a9b
baa184c
eb89bf7
aa6bdec
8b32960
713bb1b
d130d05
e949d4d
21d1dc3
b55cf05
0024074
deb0284
b09b40a
86e817d
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 |
---|---|---|
|
@@ -2,11 +2,11 @@ | |
public enum HTTPBodyStreamStrategy { | ||
/// The HTTP request's body will be collected into memory before the route handler is | ||
/// called. The max size will determine how much data can be collected. The default is | ||
/// `1 << 14`. | ||
/// 1,000,000 bytes, or 1 megabyte. | ||
/// | ||
/// See `collect(maxSize:)` to set a lower max body size. | ||
public static var collect: HTTPBodyStreamStrategy { | ||
return .collect(maxSize: 1 << 14) | ||
return .collect(maxSize: nil) | ||
} | ||
|
||
/// The HTTP request's body will not be collected first before the route handler is called | ||
|
@@ -18,7 +18,7 @@ public enum HTTPBodyStreamStrategy { | |
/// | ||
/// If a `maxSize` is supplied, the request body size in bytes will be limited. Requests | ||
/// exceeding that size will result in an error. | ||
case collect(maxSize: Int?) | ||
case collect(maxSize: ByteCount?) | ||
} | ||
|
||
extension RoutesBuilder { | ||
|
@@ -145,16 +145,28 @@ extension RoutesBuilder { | |
) -> Route | ||
where Response: ResponseEncodable | ||
{ | ||
let streamStrategy = body | ||
let responder = BasicResponder { request in | ||
if case .collect(let max) = body, request.body.data == nil { | ||
return request.body.collect(max: max).flatMapThrowing { _ in | ||
switch streamStrategy { | ||
case let .collect(maxSize): | ||
let collected: EventLoopFuture<Void> | ||
|
||
if let data = request.body.data { | ||
collected = request.eventLoop.tryFuture { | ||
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. indentation |
||
if let max = maxSize, data.readableBytes > max.value { throw Abort(.payloadTooLarge) } | ||
} | ||
} else { | ||
collected = request.body.collect(max: maxSize?.value).transform(to: ()) | ||
} | ||
|
||
return collected.flatMapThrowing { | ||
return try closure(request) | ||
}.encodeResponse(for: request) | ||
} else { | ||
return try closure(request) | ||
.encodeResponse(for: request) | ||
case .stream: | ||
return try closure(request).encodeResponse(for: request) | ||
} | ||
} | ||
|
||
let route = Route( | ||
method: method, | ||
path: path, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,13 @@ | ||
public protocol RoutesBuilder { | ||
var defaultMaxBodySize: Int? { get } | ||
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. I'm not super happy that I have to add this (at least we can have a default value). If there are any ideas on how it can be removed, I'll take them. 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. Agreed. Let me take a stab at this and see if I can get it to work without this requirement. |
||
|
||
func add(_ route: Route) | ||
} | ||
|
||
extension RoutesBuilder { | ||
public var defaultMaxBodySize: Int? { 1_000_000 } | ||
} | ||
|
||
extension UUID: LosslessStringConvertible { | ||
public init?(_ description: String) { | ||
self.init(uuidString: description) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// | ||
// ByteCount.swift | ||
// | ||
// | ||
// Created by Jimmy McDermott on 4/24/20. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Represents a number of bytes: | ||
/// | ||
/// let bytes: ByteCount = "1mb" | ||
/// print(bytes.value) // 1048576 | ||
/// | ||
/// let bytes: ByteCount = 1_000_000 | ||
/// print(bytes.value) // 1000000 | ||
|
||
/// let bytes: ByteCount = "2kib" | ||
/// print(bytes.value) // 2048 | ||
public struct ByteCount { | ||
|
||
/// The value in Bytes | ||
public let value: Int | ||
} | ||
|
||
extension ByteCount: ExpressibleByIntegerLiteral { | ||
|
||
/// Initializes the `ByteCount` with the raw byte count | ||
/// - Parameter value: The number of bytes | ||
public init(integerLiteral value: Int) { | ||
self.value = value | ||
} | ||
} | ||
|
||
extension ByteCount: ExpressibleByStringLiteral { | ||
|
||
/// Initializes the `ByteCount` via a descriptive string. Available suffixes are: | ||
/// `kib`, `mb`, `gb`, `tb` | ||
/// - Parameter value: The string value (`1mb`) | ||
public init(stringLiteral value: String) { | ||
// Short path if it's an int wrapped in a string | ||
if let intValue = Int(value) { | ||
self.value = intValue | ||
return | ||
} | ||
|
||
let validSuffixes = [ | ||
"kib": 10, | ||
"mb": 20, | ||
"gb": 30, | ||
"tb": 40 | ||
] | ||
|
||
for suffix in validSuffixes { | ||
guard value.hasSuffix(suffix.key) else { continue } | ||
guard let stringIntValue = value.components(separatedBy: suffix.key).first else { | ||
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 make this a little fuzzier? Such as lowercasing the input before splitting it. 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. Agreed, lowercasing is a good idea. Maybe removing whitespace too? |
||
fatalError("Invalid string format") | ||
} | ||
|
||
guard let intValue = Int(stringIntValue) else { | ||
fatalError("Invalid int value: \(stringIntValue)") | ||
} | ||
|
||
self.value = intValue << suffix.value | ||
return | ||
} | ||
|
||
// Assert failure here because all cases are handled in the above loop | ||
fatalError("Could not parse byte count string: \(value)") | ||
} | ||
} |
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 don't think we need this check here. The point of
max
is to avoid allocating too much memory. In this case, the memory has already been allocated we are just passing them a reference.