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 8 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 |
---|---|---|
@@ -1,11 +1,13 @@ | ||
public final class Routes: RoutesBuilder, CustomStringConvertible { | ||
public var defaultMaxBodySize: Int? | ||
public var all: [Route] | ||
|
||
public var description: String { | ||
return self.all.description | ||
} | ||
|
||
public init() { | ||
self.defaultMaxBodySize = 1_000_000 | ||
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 do |
||
self.all = [] | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,7 @@ extension RoutesBuilder { | |
/// - path: Group path components separated by commas. | ||
/// - returns: Newly created `Router` wrapped in the path. | ||
public func grouped(_ path: PathComponent...) -> RoutesBuilder { | ||
return HTTPRoutesGroup(root: self, path: path) | ||
return HTTPRoutesGroup(root: self, path: path, defaultMaxBodySize: self.defaultMaxBodySize) | ||
} | ||
|
||
/// Creates a new `Router` that will automatically prepend the supplied path components. | ||
|
@@ -29,7 +29,36 @@ extension RoutesBuilder { | |
/// - path: Group path components separated by commas. | ||
/// - configure: Closure to configure the newly created `Router`. | ||
public func group(_ path: PathComponent..., configure: (RoutesBuilder) throws -> ()) rethrows { | ||
try configure(HTTPRoutesGroup(root: self, path: path)) | ||
try configure(HTTPRoutesGroup(root: self, path: path, defaultMaxBodySize: self.defaultMaxBodySize)) | ||
} | ||
|
||
/// Creates a new `Router` with a different default max body size for its routes than the rest of the application. | ||
/// | ||
/// let large = router.group(maxSize: 1_000_000) | ||
/// large.post("image", use: self.uploadImage) | ||
/// | ||
/// - parameters: | ||
/// - defaultMaxBodySize: The maximum number of bytes that a request body can contain in the new group | ||
/// `nil` means there is no limit. | ||
/// - returns: A newly created `Router` with a new max body size. | ||
public func group(maxSize defaultMaxBodySize: Int?) -> RoutesBuilder { | ||
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 think we should use the full 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. This should be |
||
return HTTPRoutesGroup(root: self, defaultMaxBodySize: defaultMaxBodySize) | ||
} | ||
|
||
/// Creates a new `Router` with a different default max body size for its routes than the rest of the application. | ||
/// | ||
/// router.grouped(maxSize: 1_000_000) { large | ||
/// large.post("image", use: self.uploadImage) | ||
/// } | ||
/// | ||
/// - parameters: | ||
/// - defaultMaxBodySize: The maximum number of bytes that a request body can contain in the new group | ||
/// `nil` means there is no limit. | ||
/// - configure: Closure to configure the newly created `Router`. | ||
/// - builder: The new builder with the new max body size. | ||
/// - returns: A newly created `Router` with a new max body size. | ||
public func grouped(maxSize defaultMaxBodySize: Int?, configure: (_ builder: RoutesBuilder) throws -> ()) rethrows { | ||
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. This should be |
||
try configure(HTTPRoutesGroup(root: self, defaultMaxBodySize: defaultMaxBodySize)) | ||
} | ||
} | ||
|
||
|
@@ -40,11 +69,15 @@ private final class HTTPRoutesGroup: RoutesBuilder { | |
|
||
/// Additional components. | ||
let path: [PathComponent] | ||
|
||
|
||
/// The default max body size for requests in the group. | ||
let defaultMaxBodySize: Int? | ||
|
||
/// Creates a new `PathGroup`. | ||
init(root: RoutesBuilder, path: [PathComponent]) { | ||
init(root: RoutesBuilder, path: [PathComponent] = [], defaultMaxBodySize: Int? = nil) { | ||
self.root = root | ||
self.path = path | ||
self.defaultMaxBodySize = defaultMaxBodySize | ||
} | ||
|
||
/// See `HTTPRoutesBuilder`. | ||
|
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: 1_000_000) | ||
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. This should go back to being 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. Isn't 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 was thinking enum MaxSize {
// Defer to application config
case default
// Provide a custom value
// - warning: Passing `nil` here means no restrictions on how much memory an incoming request can use.
case override(ByteCount?)
} |
||
} | ||
|
||
/// The HTTP request's body will not be collected first before the route handler is called | ||
|
@@ -126,7 +126,7 @@ extension RoutesBuilder { | |
public func on<Response>( | ||
_ method: HTTPMethod, | ||
_ path: PathComponent..., | ||
body: HTTPBodyStreamStrategy = .collect, | ||
body: HTTPBodyStreamStrategy? = nil, | ||
use closure: @escaping (Request) throws -> Response | ||
) -> Route | ||
where Response: ResponseEncodable | ||
|
@@ -140,21 +140,33 @@ extension RoutesBuilder { | |
public func on<Response>( | ||
_ method: HTTPMethod, | ||
_ path: [PathComponent], | ||
body: HTTPBodyStreamStrategy = .collect, | ||
body: HTTPBodyStreamStrategy? = nil, | ||
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. We should keep this as 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. If we make this 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. Adding to #2312 (comment), my previous comments were intending that I don't think optionality makes sense for |
||
use closure: @escaping (Request) throws -> Response | ||
) -> Route | ||
where Response: ResponseEncodable | ||
{ | ||
let streamStrategy = body ?? .collect(maxSize: self.defaultMaxBodySize) | ||
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 { throw Abort(.payloadTooLarge) } | ||
} | ||
} else { | ||
collected = request.body.collect(max: maxSize).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) | ||
|
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.