Skip to content

Commit

Permalink
Don't collate the request body by default
Browse files Browse the repository at this point in the history
Only collate it when we need to ie when using a decoder
  • Loading branch information
adam-fowler committed Nov 23, 2023
1 parent 895af49 commit 1277b3a
Show file tree
Hide file tree
Showing 16 changed files with 59 additions and 86 deletions.
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Codable/CodableProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public protocol HBRequestDecoder: Sendable {
/// - Parameters:
/// - type: type to decode to
/// - request: request
func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) throws -> T
func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) async throws -> T
}

/// Default encoder. Outputs request with the swift string description of object
Expand Down
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Codable/RequestDecodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension HBRequestDecodable {
/// Create using `Codable` interfaces
/// - Parameter request: request
/// - Throws: HBHTTPError
public init(from request: HBRequest, context: some HBBaseRequestContext) throws {
self = try request.decode(as: Self.self, using: context)
public init(from request: HBRequest, context: some HBBaseRequestContext) async throws {
self = try await request.decode(as: Self.self, using: context)
}
}
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Router/RouteHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
/// ```
public protocol HBRouteHandler {
associatedtype _Output
init(from: HBRequest, context: some HBBaseRequestContext) throws
init(from: HBRequest, context: some HBBaseRequestContext) async throws
func handle(request: HBRequest, context: some HBBaseRequestContext) async throws -> _Output
}

Expand All @@ -52,7 +52,7 @@ extension HBRouterMethods {
use handlerType: Handler.Type
) -> Self where Handler._Output == _Output {
return self.on(path, method: method, options: options) { request, context -> _Output in
let handler = try Handler(from: request, context: context)
let handler = try await Handler(from: request, context: context)
return try await handler.handle(request: request, context: context)
}
}
Expand Down
46 changes: 16 additions & 30 deletions Sources/Hummingbird/Router/RouterMethods.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ public struct HBRouterMethodOptions: OptionSet, Sendable {
public init(rawValue: Int) {
self.rawValue = rawValue
}

/// don't collate the request body, expect handler to stream it
public static let streamBody: HBRouterMethodOptions = .init(rawValue: 1 << 0)
}

/// Conform to `HBRouterMethods` to add standard router verb (get, post ...) methods
Expand All @@ -45,77 +42,66 @@ public protocol HBRouterMethods {

extension HBRouterMethods {
/// GET path for async closure returning type conforming to ResponseEncodable
@discardableResult public func get<Output: HBResponseGenerator>(
@discardableResult public func get(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .GET, options: options, use: handler)
}

/// PUT path for async closure returning type conforming to ResponseEncodable
@discardableResult public func put<Output: HBResponseGenerator>(
@discardableResult public func put(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .PUT, options: options, use: handler)
}

/// DELETE path for async closure returning type conforming to ResponseEncodable
@discardableResult public func delete<Output: HBResponseGenerator>(
@discardableResult public func delete(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .DELETE, options: options, use: handler)
}

/// HEAD path for async closure returning type conforming to ResponseEncodable
@discardableResult public func head<Output: HBResponseGenerator>(
@discardableResult public func head(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .HEAD, options: options, use: handler)
}

/// POST path for async closure returning type conforming to ResponseEncodable
@discardableResult public func post<Output: HBResponseGenerator>(
@discardableResult public func post(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .POST, options: options, use: handler)
}

/// PATCH path for async closure returning type conforming to ResponseEncodable
@discardableResult public func patch<Output: HBResponseGenerator>(
@discardableResult public func patch(
_ path: String = "",
options: HBRouterMethodOptions = [],
use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output
use handler: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> Self {
return on(path, method: .PATCH, options: options, use: handler)
}

func constructResponder<Output: HBResponseGenerator>(
func constructResponder(
options: HBRouterMethodOptions,
use closure: @Sendable @escaping (HBRequest, Context) async throws -> Output
use closure: @Sendable @escaping (HBRequest, Context) async throws -> some HBResponseGenerator
) -> HBCallbackResponder<Context> {
return HBCallbackResponder { request, context in
if options.contains(.streamBody) {
let output = try await closure(request, context)
return try output.response(from: request, context: context)
} else {
var request = request
do {
request.body = try await request.body.collate(maxSize: context.applicationContext.configuration.maxUploadSize)
} catch {
throw HBHTTPError(.payloadTooLarge)
}
let output = try await closure(request, context)
return try output.response(from: request, context: context)
}
let output = try await closure(request, context)
return try output.response(from: request, context: context)
}
}
}
4 changes: 2 additions & 2 deletions Sources/Hummingbird/Server/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ public struct HBRequest: Sendable {

/// Decode request using decoder stored at `HBApplication.decoder`.
/// - Parameter type: Type you want to decode to
public func decode<Type: Decodable>(as type: Type.Type, using context: some HBBaseRequestContext) throws -> Type {
public func decode<Type: Decodable>(as type: Type.Type, using context: some HBBaseRequestContext) async throws -> Type {
do {
return try context.applicationContext.decoder.decode(type, from: self, context: context)
return try await context.applicationContext.decoder.decode(type, from: self, context: context)
} catch DecodingError.dataCorrupted(_) {
let message = "The given data was not valid input."
throw HBHTTPError(.badRequest, message: message)
Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdCore/Request/RequestBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public enum HBRequestBody: Sendable, AsyncSequence {
case .byteBuffer:
return self
case .stream(let streamer):
return try .byteBuffer(await streamer.collect(upTo: maxSize))
return try await .byteBuffer(streamer.collect(upTo: maxSize))
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions Sources/HummingbirdCore/Server/HTTP/HTTPChannelHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension HTTPChannelHandler {
do {
try await withGracefulShutdownHandler {
try await withThrowingTaskGroup(of: Void.self) { group in
try await asyncChannel.executeThenClose { inbound, outbound in
try await asyncChannel.executeThenClose { inbound, outbound in
let responseWriter = HBHTTPServerBodyWriter(outbound: outbound)
var iterator = inbound.makeAsyncIterator()
while let part = try await iterator.next() {
Expand Down Expand Up @@ -98,7 +98,7 @@ extension HTTPChannelHandler {
} onGracefulShutdown: {
// set to cancelled
if processingRequest.exchange(.cancelled, ordering: .relaxed) == .idle {
// only close the channel input if it is idle
// only close the channel input if it is idle
asyncChannel.channel.close(mode: .input, promise: nil)
}
}
Expand Down Expand Up @@ -137,3 +137,10 @@ struct HBHTTPServerBodyWriter: Sendable, HBResponseBodyWriter {
try await self.outbound.write(.body(buffer))
}
}

// If we catch a too many bytes error report that as payload too large
extension NIOTooManyBytesError: HBHTTPResponseError {
public var status: NIOHTTP1.HTTPResponseStatus { .payloadTooLarge }
public var headers: NIOHTTP1.HTTPHeaders { [:] }
public func body(allocator: NIOCore.ByteBufferAllocator) -> NIOCore.ByteBuffer? { nil }
}
10 changes: 3 additions & 7 deletions Sources/HummingbirdFoundation/Codable/JSON/JSONCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@ extension JSONDecoder: HBRequestDecoder {
/// - Parameters:
/// - type: Type to decode
/// - request: Request to decode from
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) throws -> T {
guard case .byteBuffer(var buffer) = request.body,
let data = buffer.readData(length: buffer.readableBytes)
else {
throw HBHTTPError(.badRequest)
}
return try self.decode(T.self, from: data)
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) async throws -> T {
let buffer = try await request.body.collect(upTo: context.applicationContext.configuration.maxUploadSize)
return try self.decode(T.self, from: buffer)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ extension URLEncodedFormDecoder: HBRequestDecoder {
/// - Parameters:
/// - type: Type to decode
/// - request: Request to decode from
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) throws -> T {
guard case .byteBuffer(var buffer) = request.body,
let string = buffer.readString(length: buffer.readableBytes)
else {
throw HBHTTPError(.badRequest)
}
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest, context: some HBBaseRequestContext) async throws -> T {
let buffer = try await request.body.collect(upTo: context.applicationContext.configuration.maxUploadSize)
let string = String(buffer: buffer)
return try self.decode(T.self, from: string)
}
}
2 changes: 1 addition & 1 deletion Sources/PerformanceTest/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ router.get { _, _ in

// request with a body
// ./wrk -c 128 -d 15s -t 8 -s scripts/post.lua http://localhost:8080
router.post(options: .streamBody) { request, _ in
router.post { request, _ in
return HBResponse(status: .ok, body: .init(asyncSequence: request.body))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class HummingbirdJSONTests: XCTestCase {
func testDecode() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.put("/user") { request, context -> HTTPResponseStatus in
guard let user = try? request.decode(as: User.self, using: context) else { throw HBHTTPError(.badRequest) }
guard let user = try? await request.decode(as: User.self, using: context) else { throw HBHTTPError(.badRequest) }
XCTAssertEqual(user.name, "John Smith")
XCTAssertEqual(user.email, "john.smith@email.com")
XCTAssertEqual(user.age, 25)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class HummingBirdURLEncodedTests: XCTestCase {
func testDecode() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.put("/user") { request, context -> HTTPResponseStatus in
guard let user = try? request.decode(as: User.self, using: context) else { throw HBHTTPError(.badRequest) }
guard let user = try? await request.decode(as: User.self, using: context) else { throw HBHTTPError(.badRequest) }
XCTAssertEqual(user.name, "John Smith")
XCTAssertEqual(user.email, "john.smith@email.com")
XCTAssertEqual(user.age, 25)
Expand Down
33 changes: 10 additions & 23 deletions Tests/HummingbirdTests/ApplicationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,26 +194,12 @@ final class ApplicationTests: XCTestCase {
}
}

func testEventLoopFutureArray() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.patch("array") { _, _ -> [String] in
return ["yes", "no"]
}
let app = HBApplication(responder: router.buildResponder())
try await app.test(.router) { client in
try await client.XCTExecute(uri: "/array", method: .PATCH) { response in
let body = try XCTUnwrap(response.body)
XCTAssertEqual(String(buffer: body), "[\"yes\", \"no\"]")
}
}
}

func testResponseBody() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router
.group("/echo-body")
.post { request, _ -> HBResponse in
guard case .byteBuffer(let buffer) = request.body else { return .init(status: .ok) }
let buffer = try await request.body.collect(upTo: .max)
return .init(status: .ok, headers: [:], body: .init(byteBuffer: buffer))
}
let app = HBApplication(responder: router.buildResponder(), configuration: .init(maxUploadSize: 2 * 1024 * 1024))
Expand All @@ -230,10 +216,10 @@ final class ApplicationTests: XCTestCase {
/// Test streaming of requests and streaming of responses by streaming the request body into a response streamer
func testStreaming() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.post("streaming", options: .streamBody) { request, _ -> HBResponse in
router.post("streaming") { request, _ -> HBResponse in
return HBResponse(status: .ok, body: .init(asyncSequence: request.body))
}
router.post("size", options: .streamBody) { request, _ -> String in
router.post("size") { request, _ -> String in
var size = 0
for try await buffer in request.body {
size += buffer.readableBytes
Expand Down Expand Up @@ -263,7 +249,7 @@ final class ApplicationTests: XCTestCase {
/// Test streaming of requests and streaming of responses by streaming the request body into a response streamer
func testStreamingSmallBuffer() async throws {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.post("streaming", options: .streamBody) { request, _ -> HBResponse in
router.post("streaming") { request, _ -> HBResponse in
return HBResponse(status: .ok, body: .init(asyncSequence: request.body))
}
let app = HBApplication(responder: router.buildResponder())
Expand Down Expand Up @@ -310,8 +296,8 @@ final class ApplicationTests: XCTestCase {
router
.group("/echo-body")
.post { request, _ -> ByteBuffer? in
guard case .byteBuffer(let buffer) = request.body, buffer.readableBytes > 0 else { return nil }
return buffer
let buffer = try await request.body.collect(upTo: .max)
return buffer.readableBytes > 0 ? buffer : nil
}
let app = HBApplication(responder: router.buildResponder())
try await app.test(.router) { client in
Expand Down Expand Up @@ -402,10 +388,11 @@ final class ApplicationTests: XCTestCase {

func testMaxUploadSize() async throws {
let router = HBRouterBuilder()
router.post("upload") { _, _ in
"ok"
router.post("upload") { request, context in
_ = try await request.body.collate(maxSize: context.applicationContext.configuration.maxUploadSize)
return "ok"
}
router.post("stream", options: .streamBody) { _, _ in
router.post("stream") { _, _ in
"ok"
}
let app = HBApplication(responder: router.buildResponder(), configuration: .init(maxUploadSize: 64 * 1024))
Expand Down
2 changes: 1 addition & 1 deletion Tests/HummingbirdTests/FileIOTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class FileIOTests: XCTestCase {
func testWriteLargeFile() async throws {
let filename = "testWriteLargeFile.txt"
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.put("store", options: .streamBody) { request, context -> HTTPResponseStatus in
router.put("store") { request, context -> HTTPResponseStatus in
let fileIO = HBFileIO(threadPool: context.threadPool)
try await fileIO.writeFile(contents: request.body, path: filename, context: context, logger: context.logger)
return .ok
Expand Down
2 changes: 1 addition & 1 deletion Tests/HummingbirdTests/MiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ final class MiddlewareTests: XCTestCase {
let router = HBRouterBuilder(context: HBTestRouterContext.self)
router.group()
.add(middleware: TransformMiddleware())
.get("test", options: .streamBody) { request, _ in
.get("test") { request, _ in
return HBResponse(status: .ok, body: .init(asyncSequence: request.body))
}
let app = HBApplication(responder: router.buildResponder())
Expand Down
Loading

0 comments on commit 1277b3a

Please sign in to comment.