From 0249c998c176952cab64c05befce78b5cbf25eb7 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 29 Oct 2023 19:01:11 +0100 Subject: [PATCH 1/8] Transition HBResponder to be async/await, removing HBAsyncResponder in the process This PR provides a compatibility helper for APIs still working with ELFs, primarily the Middleware --- .../AsyncAwaitSupport/AsyncMiddleware.swift | 8 +- .../AsyncAwaitSupport/AsyncResponder.swift | 44 ----- .../RouteHandler+async.swift | 32 ---- .../AsyncAwaitSupport/Router+async.swift | 119 ------------- .../Middleware/CORSMiddleware.swift | 54 +++--- .../Middleware/MetricsMiddleware.swift | 15 +- .../Hummingbird/Middleware/Middleware.swift | 17 +- .../Middleware/TracingMiddleware.swift | 120 ++++++------- Sources/Hummingbird/Router/RouteHandler.swift | 76 +-------- Sources/Hummingbird/Router/Router.swift | 6 +- .../Hummingbird/Router/RouterBuilder.swift | 20 +-- Sources/Hummingbird/Router/RouterGroup.swift | 17 +- .../Hummingbird/Router/RouterMethods.swift | 160 +++--------------- .../Server/Application+HTTPResponder.swift | 5 +- .../Hummingbird/Server/RequestContext.swift | 9 +- Sources/Hummingbird/Server/Responder.swift | 13 +- .../Hummingbird/Server/ServiceContext.swift | 70 +------- .../TemporaryELFSupport/HBResponder+ELF.swift | 10 ++ .../FilesTests.swift | 12 +- Tests/HummingbirdTests/ApplicationTests.swift | 38 ++--- Tests/HummingbirdTests/AsyncAwaitTests.swift | 2 +- Tests/HummingbirdTests/HandlerTests.swift | 4 +- Tests/HummingbirdTests/MiddlewareTests.swift | 13 +- Tests/HummingbirdTests/PersistTests.swift | 82 ++++----- Tests/HummingbirdTests/RouterTests.swift | 6 +- Tests/HummingbirdTests/TracingTests.swift | 19 ++- documentation/Encoding and Decoding.md | 2 +- documentation/Error Handling.md | 2 +- documentation/Router.md | 2 +- 29 files changed, 269 insertions(+), 708 deletions(-) delete mode 100644 Sources/Hummingbird/AsyncAwaitSupport/AsyncResponder.swift delete mode 100644 Sources/Hummingbird/AsyncAwaitSupport/RouteHandler+async.swift delete mode 100644 Sources/Hummingbird/AsyncAwaitSupport/Router+async.swift create mode 100644 Sources/Hummingbird/TemporaryELFSupport/HBResponder+ELF.swift diff --git a/Sources/Hummingbird/AsyncAwaitSupport/AsyncMiddleware.swift b/Sources/Hummingbird/AsyncAwaitSupport/AsyncMiddleware.swift index 9711634e2..82a1d38fe 100644 --- a/Sources/Hummingbird/AsyncAwaitSupport/AsyncMiddleware.swift +++ b/Sources/Hummingbird/AsyncAwaitSupport/AsyncMiddleware.swift @@ -51,13 +51,13 @@ struct HBPropagateServiceContextResponder: HBR let responder: any HBResponder let context: Context - func respond(to request: HBRequest, context: Context) -> EventLoopFuture { + func respond(to request: HBRequest, context: Context) async throws -> HBResponse { if let serviceContext = ServiceContext.$current.get() { - return context.withServiceContext(serviceContext) { context in - self.responder.respond(to: request, context: context) + return try await context.withServiceContext(serviceContext) { context in + try await self.responder.respond(to: request, context: context) } } else { - return self.responder.respond(to: request, context: context) + return try await self.responder.respond(to: request, context: context) } } } diff --git a/Sources/Hummingbird/AsyncAwaitSupport/AsyncResponder.swift b/Sources/Hummingbird/AsyncAwaitSupport/AsyncResponder.swift deleted file mode 100644 index 7921a48c7..000000000 --- a/Sources/Hummingbird/AsyncAwaitSupport/AsyncResponder.swift +++ /dev/null @@ -1,44 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIO -import ServiceContextModule - -extension HBResponder { - /// extend HBResponder to provide async/await version of respond - public func respond(to request: HBRequest, context: Context) async throws -> HBResponse { - return try await self.respond(to: request, context: context).get() - } -} - -/// Responder that calls supplied closure -public struct HBAsyncCallbackResponder: HBResponder { - let callback: @Sendable (HBRequest, Context) async throws -> HBResponse - - public init(callback: @escaping @Sendable (HBRequest, Context) async throws -> HBResponse) { - self.callback = callback - } -} - -public extension HBAsyncCallbackResponder where Context: HBRequestContext { - func respond(to request: HBRequest, context: Context) -> EventLoopFuture { - let promise = context.eventLoop.makePromise(of: HBResponse.self) - promise.completeWithTask { - return try await ServiceContext.$current.withValue(context.serviceContext) { - try await self.callback(request, context) - } - } - return promise.futureResult - } -} diff --git a/Sources/Hummingbird/AsyncAwaitSupport/RouteHandler+async.swift b/Sources/Hummingbird/AsyncAwaitSupport/RouteHandler+async.swift deleted file mode 100644 index 76a7e6a0b..000000000 --- a/Sources/Hummingbird/AsyncAwaitSupport/RouteHandler+async.swift +++ /dev/null @@ -1,32 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// Route Handler using async/await methods -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public protocol HBAsyncRouteHandler: HBRouteHandler where _Output == EventLoopFuture<_Output2> { - associatedtype _Output2 - init(from: HBRequest, context: HBRequestContext) throws - func handle(request: HBRequest, context: HBRequestContext) async throws -> _Output2 -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension HBAsyncRouteHandler { - public func handle(request: HBRequest, context: HBRequestContext) throws -> EventLoopFuture<_Output2> { - let promise = context.eventLoop.makePromise(of: _Output2.self) - promise.completeWithTask { - try await self.handle(request: request, context: context) - } - return promise.futureResult - } -} diff --git a/Sources/Hummingbird/AsyncAwaitSupport/Router+async.swift b/Sources/Hummingbird/AsyncAwaitSupport/Router+async.swift deleted file mode 100644 index de27b3efa..000000000 --- a/Sources/Hummingbird/AsyncAwaitSupport/Router+async.swift +++ /dev/null @@ -1,119 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2023 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import NIOCore - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension HBRouterMethods { - /// GET path for async closure returning type conforming to ResponseEncodable - @discardableResult public func get( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .GET, options: options, use: handler) - } - - /// PUT path for async closure returning type conforming to ResponseEncodable - @discardableResult public func put( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .PUT, options: options, use: handler) - } - - /// DELETE path for async closure returning type conforming to ResponseEncodable - @discardableResult public func delete( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .DELETE, options: options, use: handler) - } - - /// HEAD path for async closure returning type conforming to ResponseEncodable - @discardableResult public func head( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .HEAD, options: options, use: handler) - } - - /// POST path for async closure returning type conforming to ResponseEncodable - @discardableResult public func post( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .POST, options: options, use: handler) - } - - /// PATCH path for async closure returning type conforming to ResponseEncodable - @discardableResult public func patch( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - return on(path, method: .PATCH, options: options, use: handler) - } - - func constructResponder( - options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) async throws -> Output - ) -> any HBResponder { - return HBAsyncCallbackResponder { request, context in - var request = request - if case .stream = request.body, !options.contains(.streamBody) { - let buffer = try await request.body.consumeBody( - maxSize: context.applicationContext.configuration.maxUploadSize - ) - request.body = .byteBuffer(buffer) - } - return try await closure(request, context).response(from: request, context: context) - } - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension HBRouterBuilder { - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func on( - _ path: String, - method: HTTPMethod, - options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - let responder = constructResponder(options: options, use: closure) - add(path, method: method, responder: responder) - return self - } -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension HBRouterGroup { - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func on( - _ path: String = "", - method: HTTPMethod, - options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) async throws -> Output - ) -> Self { - let responder = constructResponder(options: options, use: closure) - let path = self.combinePaths(self.path, path) - self.router.add(path, method: method, responder: self.middlewares.constructResponder(finalResponder: responder)) - return self - } -} diff --git a/Sources/Hummingbird/Middleware/CORSMiddleware.swift b/Sources/Hummingbird/Middleware/CORSMiddleware.swift index c5246e73b..918643119 100644 --- a/Sources/Hummingbird/Middleware/CORSMiddleware.swift +++ b/Sources/Hummingbird/Middleware/CORSMiddleware.swift @@ -84,34 +84,36 @@ public struct HBCORSMiddleware: HBMiddleware { /// apply CORS middleware public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { - // if no origin header then don't apply CORS - guard request.headers["origin"].first != nil else { return next.respond(to: request, context: context) } - - if request.method == .OPTIONS { - // if request is OPTIONS then return CORS headers and skip the rest of the middleware chain - var headers: HTTPHeaders = [ - "access-control-allow-origin": allowOrigin.value(for: request) ?? "", - ] - headers.add(name: "access-control-allow-headers", value: self.allowHeaders) - headers.add(name: "access-control-allow-methods", value: self.allowMethods) - if self.allowCredentials { - headers.add(name: "access-control-allow-credentials", value: "true") - } - if let maxAge = self.maxAge { - headers.add(name: "access-control-max-age", value: maxAge) - } - if let exposedHeaders = self.exposedHeaders { - headers.add(name: "access-control-expose-headers", value: exposedHeaders) - } - if case .originBased = self.allowOrigin { - headers.add(name: "vary", value: "Origin") + context.eventLoop.makeFutureWithTask { + // if no origin header then don't apply CORS + guard request.headers["origin"].first != nil else { + return try await next.respond(to: request, context: context) } - return context.success(HBResponse(status: .noContent, headers: headers, body: .empty)) - } else { - // if not OPTIONS then run rest of middleware chain and add origin value at the end - return next.respond(to: request, context: context).map { response in - var response = response + if request.method == .OPTIONS { + // if request is OPTIONS then return CORS headers and skip the rest of the middleware chain + var headers: HTTPHeaders = [ + "access-control-allow-origin": allowOrigin.value(for: request) ?? "", + ] + headers.add(name: "access-control-allow-headers", value: self.allowHeaders) + headers.add(name: "access-control-allow-methods", value: self.allowMethods) + if self.allowCredentials { + headers.add(name: "access-control-allow-credentials", value: "true") + } + if let maxAge = self.maxAge { + headers.add(name: "access-control-max-age", value: maxAge) + } + if let exposedHeaders = self.exposedHeaders { + headers.add(name: "access-control-expose-headers", value: exposedHeaders) + } + if case .originBased = self.allowOrigin { + headers.add(name: "vary", value: "Origin") + } + + return HBResponse(status: .noContent, headers: headers, body: .empty) + } else { + // if not OPTIONS then run rest of middleware chain and add origin value at the end + var response = try await next.respond(to: request, context: context) response.headers.add(name: "access-control-allow-origin", value: self.allowOrigin.value(for: request) ?? "") if self.allowCredentials { response.headers.add(name: "access-control-allow-credentials", value: "true") diff --git a/Sources/Hummingbird/Middleware/MetricsMiddleware.swift b/Sources/Hummingbird/Middleware/MetricsMiddleware.swift index 7d5518d4c..bb6bf95d2 100644 --- a/Sources/Hummingbird/Middleware/MetricsMiddleware.swift +++ b/Sources/Hummingbird/Middleware/MetricsMiddleware.swift @@ -25,10 +25,10 @@ public struct HBMetricsMiddleware: HBMiddleware { public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { let startTime = DispatchTime.now().uptimeNanoseconds - let responseFuture = next.respond(to: request, context: context) - responseFuture.whenComplete { result in - switch result { - case .success: + let promise = context.eventLoop.makePromise(of: HBResponse.self) + promise.completeWithTask { + do { + let response = try await next.respond(to: request, context: context) // need to create dimensions once request has been responded to ensure // we have the correct endpoint path let dimensions: [(String, String)] = [ @@ -41,8 +41,8 @@ public struct HBMetricsMiddleware: HBMiddleware { dimensions: dimensions, preferredDisplayUnit: .seconds ).recordNanoseconds(DispatchTime.now().uptimeNanoseconds - startTime) - - case .failure: + return response + } catch { // need to create dimensions once request has been responded to ensure // we have the correct endpoint path let dimensions: [(String, String)] @@ -59,8 +59,9 @@ public struct HBMetricsMiddleware: HBMiddleware { ] } Counter(label: "hb_errors", dimensions: dimensions).increment() + throw error } } - return responseFuture + return promise.futureResult } } diff --git a/Sources/Hummingbird/Middleware/Middleware.swift b/Sources/Hummingbird/Middleware/Middleware.swift index 5b645991c..26b8827f2 100644 --- a/Sources/Hummingbird/Middleware/Middleware.swift +++ b/Sources/Hummingbird/Middleware/Middleware.swift @@ -23,20 +23,19 @@ import NIOCore /// Middleware allows you to process a request before it reaches your request handler and then process the response /// returned by that handler. /// ``` -/// func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture { +/// func apply(to request: HBRequest, next: HBResponder) async throws -> HBResponse { /// let request = processRequest(request) -/// return next.respond(to: request).map { response in -/// return processResponse(response) -/// } +/// let response = try await next.respond(to: request) +/// return processResponse(response) /// } /// ``` /// Middleware also allows you to shortcut the whole process and not pass on the request to the handler /// ``` -/// func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture { +/// func apply(to request: HBRequest, next: HBResponder) async throws -> HBResponse { /// if request.method == .OPTIONS { -/// return context.success(HBResponse(status: .noContent)) +/// return HBResponse(status: .noContent) /// } else { -/// return next.respond(to: request) +/// return try await next.respond(to: request) /// } /// } /// ``` @@ -49,7 +48,7 @@ struct MiddlewareResponder: HBResponder { let middleware: any HBMiddleware let next: any HBResponder - func respond(to request: HBRequest, context: Context) -> EventLoopFuture { - return self.middleware.apply(to: request, context: context, next: self.next) + func respond(to request: HBRequest, context: Context) async throws -> HBResponse { + return try await self.middleware.apply(to: request, context: context, next: self.next).get() } } diff --git a/Sources/Hummingbird/Middleware/TracingMiddleware.swift b/Sources/Hummingbird/Middleware/TracingMiddleware.swift index ead9a28a0..c0a2ab75c 100644 --- a/Sources/Hummingbird/Middleware/TracingMiddleware.swift +++ b/Sources/Hummingbird/Middleware/TracingMiddleware.swift @@ -38,74 +38,74 @@ public struct HBTracingMiddleware) -> EventLoopFuture { - var serviceContext = context.serviceContext - InstrumentationSystem.instrument.extract(request.headers, into: &serviceContext, using: HTTPHeadersExtractor()) + return context.eventLoop.makeFutureWithTask { + var serviceContext = context.serviceContext + InstrumentationSystem.instrument.extract(request.headers, into: &serviceContext, using: HTTPHeadersExtractor()) - let operationName: String = { - guard let endpointPath = context.endpointPath else { - return "HTTP \(request.method.rawValue) route not found" - } - return endpointPath - }() - - return context.withSpan(operationName, serviceContext: serviceContext, ofKind: .server) { context, span in - span.updateAttributes { attributes in - attributes["http.method"] = request.method.rawValue - attributes["http.target"] = request.uri.path - attributes["http.flavor"] = "\(request.version.major).\(request.version.minor)" - attributes["http.scheme"] = request.uri.scheme?.rawValue - attributes["http.user_agent"] = request.headers.first(name: "user-agent") - attributes["http.request_content_length"] = request.headers["content-length"].first.map { Int($0) } ?? nil - - attributes["net.host.name"] = context.applicationContext.configuration.address.host - attributes["net.host.port"] = context.applicationContext.configuration.address.port - - if let remoteAddress = context.remoteAddress { - attributes["net.sock.peer.port"] = remoteAddress.port - - switch remoteAddress.protocol { - case .inet: - attributes["net.sock.peer.addr"] = remoteAddress.ipAddress - case .inet6: - attributes["net.sock.family"] = "inet6" - attributes["net.sock.peer.addr"] = remoteAddress.ipAddress - case .unix: - attributes["net.sock.family"] = "unix" - attributes["net.sock.peer.addr"] = remoteAddress.pathname - default: - break + let operationName: String = { + guard let endpointPath = context.endpointPath else { + return "HTTP \(request.method.rawValue) route not found" + } + return endpointPath + }() + + return try await context.withSpan(operationName, serviceContext: serviceContext, ofKind: .server) { context, span in + span.updateAttributes { attributes in + attributes["http.method"] = request.method.rawValue + attributes["http.target"] = request.uri.path + attributes["http.flavor"] = "\(request.version.major).\(request.version.minor)" + attributes["http.scheme"] = request.uri.scheme?.rawValue + attributes["http.user_agent"] = request.headers.first(name: "user-agent") + attributes["http.request_content_length"] = request.headers["content-length"].first.map { Int($0) } ?? nil + + attributes["net.host.name"] = context.applicationContext.configuration.address.host + attributes["net.host.port"] = context.applicationContext.configuration.address.port + + if let remoteAddress = context.remoteAddress { + attributes["net.sock.peer.port"] = remoteAddress.port + + switch remoteAddress.protocol { + case .inet: + attributes["net.sock.peer.addr"] = remoteAddress.ipAddress + case .inet6: + attributes["net.sock.family"] = "inet6" + attributes["net.sock.peer.addr"] = remoteAddress.ipAddress + case .unix: + attributes["net.sock.family"] = "unix" + attributes["net.sock.peer.addr"] = remoteAddress.pathname + default: + break + } } + attributes = self.recordHeaders(request.headers, toSpanAttributes: attributes, withPrefix: "http.request.header.") } - attributes = self.recordHeaders(request.headers, toSpanAttributes: attributes, withPrefix: "http.request.header.") - } - return next.respond(to: request, context: context) - .always { result in - switch result { - case .success(let response): - span.updateAttributes { attributes in - attributes = self.recordHeaders(response.headers, toSpanAttributes: attributes, withPrefix: "http.response.header.") - - attributes["http.status_code"] = Int(response.status.code) - switch response.body { - case .byteBuffer(let buffer): - attributes["http.response_content_length"] = buffer.readableBytes - case .stream: - attributes["http.response_content_length"] = response.headers["content-length"].first.map { Int($0) } ?? nil - case .empty: - break - } + do { + let response = try await next.respond(to: request, context: context) + span.updateAttributes { attributes in + attributes = self.recordHeaders(response.headers, toSpanAttributes: attributes, withPrefix: "http.response.header.") + + attributes["http.status_code"] = Int(response.status.code) + switch response.body { + case .byteBuffer(let buffer): + attributes["http.response_content_length"] = buffer.readableBytes + case .stream: + attributes["http.response_content_length"] = response.headers["content-length"].first.map { Int($0) } ?? nil + case .empty: + break } - case .failure(let error): - if let httpError = error as? HBHTTPResponseError { - span.attributes["http.status_code"] = Int(httpError.status.code) + } + return response + } catch let error as HBHTTPResponseError { + span.attributes["http.status_code"] = Int(error.status.code) - if 500..<600 ~= httpError.status.code { - span.setStatus(.init(code: .error)) - } - } + if 500..<600 ~= error.status.code { + span.setStatus(.init(code: .error)) } + + throw error } + } } } diff --git a/Sources/Hummingbird/Router/RouteHandler.swift b/Sources/Hummingbird/Router/RouteHandler.swift index ebc9c1d97..913f2eb38 100644 --- a/Sources/Hummingbird/Router/RouteHandler.swift +++ b/Sources/Hummingbird/Router/RouteHandler.swift @@ -40,7 +40,7 @@ public protocol HBRouteHandler { associatedtype _Output init(from: HBRequest, context: HBRequestContext) throws - func handle(request: HBRequest, context: HBRequestContext) throws -> _Output + func handle(request: HBRequest, context: HBRequestContext) async throws -> _Output } extension HBRouterMethods { @@ -53,25 +53,7 @@ extension HBRouterMethods { ) -> 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) - return try handler.handle(request: request, context: context) - } - } - - /// Add path for `HBRouteHandler` that returns an `EventLoopFuture` specialized with a type conforming - /// to `HBResponseGenerator` - @discardableResult func on( - _ path: String, - method: HTTPMethod, - options: HBRouterMethodOptions = [], - use handlerType: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: method, options: options) { request, context -> EventLoopFuture<_Output> in - do { - let handler = try Handler(from: request, context: context) - return try handler.handle(request: request, context: context) - } catch { - return context.failure(error) - } + return try await handler.handle(request: request, context: context) } } @@ -128,58 +110,4 @@ extension HBRouterMethods { ) -> Self where Handler._Output == _Output { return self.on(path, method: .PATCH, options: options, use: handler) } - - /// GET path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func get( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .GET, options: options, use: handler) - } - - /// PUT path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func put( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .PUT, options: options, use: handler) - } - - /// POST path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func post( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .POST, options: options, use: handler) - } - - /// HEAD path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func head( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .HEAD, options: options, use: handler) - } - - /// DELETE path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func delete( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .DELETE, options: options, use: handler) - } - - /// PATCH path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func patch( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: Handler.Type - ) -> Self where Handler._Output == EventLoopFuture<_Output> { - return self.on(path, method: .PATCH, options: options, use: handler) - } } diff --git a/Sources/Hummingbird/Router/Router.swift b/Sources/Hummingbird/Router/Router.swift index 6420585ec..6a7dd7799 100644 --- a/Sources/Hummingbird/Router/Router.swift +++ b/Sources/Hummingbird/Router/Router.swift @@ -29,12 +29,12 @@ struct HBRouter: HBResponder { /// Respond to request by calling correct handler /// - Parameter request: HTTP request /// - Returns: EventLoopFuture that will be fulfilled with the Response - public func respond(to request: HBRequest, context: Context) -> EventLoopFuture { + public func respond(to request: HBRequest, context: Context) async throws -> HBResponse { let path = request.uri.path guard let result = trie.getValueAndParameters(path), let responder = result.value.getResponder(for: request.method) else { - return self.notFoundResponder.respond(to: request, context: context) + return try await self.notFoundResponder.respond(to: request, context: context) } var context = context if let parameters = result.parameters { @@ -42,6 +42,6 @@ struct HBRouter: HBResponder { } // store endpoint path in request (mainly for metrics) context.coreContext.endpointPath.value = result.value.path - return responder.respond(to: request, context: context) + return try await responder.respond(to: request, context: context) } } diff --git a/Sources/Hummingbird/Router/RouterBuilder.swift b/Sources/Hummingbird/Router/RouterBuilder.swift index f6dc561c2..cb01b9c09 100644 --- a/Sources/Hummingbird/Router/RouterBuilder.swift +++ b/Sources/Hummingbird/Router/RouterBuilder.swift @@ -77,25 +77,13 @@ public final class HBRouterBuilder: HBRouterMethods { _ path: String, method: HTTPMethod, options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) throws -> Output + use closure: @escaping (HBRequest, Context) async throws -> Output ) -> Self { let responder = constructResponder(options: options, use: closure) self.add(path, method: method, responder: responder) return self } - - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func on( - _ path: String, - method: HTTPMethod, - options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) -> EventLoopFuture - ) -> Self { - let responder = constructResponder(options: options, use: closure) - self.add(path, method: method, responder: responder) - return self - } - + /// return new `RouterGroup` /// - Parameter path: prefix to add to paths inside the group public func group(_ path: String = "") -> HBRouterGroup { @@ -105,7 +93,7 @@ public final class HBRouterBuilder: HBRouterMethods { /// Responder that return a not found error struct NotFoundResponder: HBResponder { - func respond(to request: HBRequest, context: Context) -> NIOCore.EventLoopFuture { - return context.eventLoop.makeFailedFuture(HBHTTPError(.notFound)) + func respond(to request: HBRequest, context: Context) async throws -> HBResponse { + throw HBHTTPError(.notFound) } } diff --git a/Sources/Hummingbird/Router/RouterGroup.swift b/Sources/Hummingbird/Router/RouterGroup.swift index bd19b2c0e..079b49a05 100644 --- a/Sources/Hummingbird/Router/RouterGroup.swift +++ b/Sources/Hummingbird/Router/RouterGroup.swift @@ -57,25 +57,12 @@ public struct HBRouterGroup: HBRouterMethods { ) } - /// Add path for closure returning type conforming to ResponseFutureEncodable + /// Add path for closure returning type using async/await @discardableResult public func on( _ path: String = "", method: HTTPMethod, options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) throws -> Output - ) -> Self { - let responder = constructResponder(options: options, use: closure) - let path = self.combinePaths(self.path, path) - self.router.add(path, method: method, responder: self.middlewares.constructResponder(finalResponder: responder)) - return self - } - - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func on( - _ path: String = "", - method: HTTPMethod, - options: HBRouterMethodOptions = [], - use closure: @escaping (HBRequest, Context) -> EventLoopFuture + use closure: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { let responder = constructResponder(options: options, use: closure) let path = self.combinePaths(self.path, path) diff --git a/Sources/Hummingbird/Router/RouterMethods.swift b/Sources/Hummingbird/Router/RouterMethods.swift index a7fd465c6..5465d19ed 100644 --- a/Sources/Hummingbird/Router/RouterMethods.swift +++ b/Sources/Hummingbird/Router/RouterMethods.swift @@ -30,22 +30,6 @@ public struct HBRouterMethodOptions: OptionSet { public protocol HBRouterMethods { associatedtype Context: HBRequestContext - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult func on( - _ path: String, - method: HTTPMethod, - options: HBRouterMethodOptions, - use: @escaping (HBRequest, Context) throws -> Output - ) -> Self - - /// Add path for closure returning type conforming to ResponseFutureEncodable - @discardableResult func on( - _ path: String, - method: HTTPMethod, - options: HBRouterMethodOptions, - use: @escaping (HBRequest, Context) -> EventLoopFuture - ) -> Self - /// Add path for async closure @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @discardableResult func on( @@ -60,184 +44,86 @@ public protocol HBRouterMethods { } extension HBRouterMethods { - /// GET path for closure returning type conforming to HBResponseGenerator + /// GET path for async closure returning type conforming to ResponseEncodable @discardableResult public func get( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .GET, options: options, use: handler) } - /// PUT path for closure returning type conforming to HBResponseGenerator + /// PUT path for async closure returning type conforming to ResponseEncodable @discardableResult public func put( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .PUT, options: options, use: handler) } - /// POST path for closure returning type conforming to HBResponseGenerator - @discardableResult public func post( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output - ) -> Self { - return on(path, method: .POST, options: options, use: handler) - } - - /// HEAD path for closure returning type conforming to HBResponseGenerator - @discardableResult public func head( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output - ) -> Self { - return on(path, method: .HEAD, options: options, use: handler) - } - - /// DELETE path for closure returning type conforming to HBResponseGenerator + /// DELETE path for async closure returning type conforming to ResponseEncodable @discardableResult public func delete( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .DELETE, options: options, use: handler) } - /// PATCH path for closure returning type conforming to HBResponseGenerator - @discardableResult public func patch( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) throws -> Output - ) -> Self { - return on(path, method: .PATCH, options: options, use: handler) - } - - /// GET path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func get( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) -> EventLoopFuture - ) -> Self { - return on(path, method: .GET, options: options, use: handler) - } - - /// PUT path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func put( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) -> EventLoopFuture - ) -> Self { - return on(path, method: .PUT, options: options, use: handler) - } - - /// DELETE path for closure returning type conforming to ResponseFutureEncodable - @discardableResult public func delete( - _ path: String = "", - options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) -> EventLoopFuture - ) -> Self { - return on(path, method: .DELETE, options: options, use: handler) - } - - /// HEAD path for closure returning type conforming to ResponseFutureEncodable + /// HEAD path for async closure returning type conforming to ResponseEncodable @discardableResult public func head( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) -> EventLoopFuture + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .HEAD, options: options, use: handler) } - /// POST path for closure returning type conforming to ResponseFutureEncodable + /// POST path for async closure returning type conforming to ResponseEncodable @discardableResult public func post( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) -> EventLoopFuture + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .POST, options: options, use: handler) } - /// PATCH path for closure returning type conforming to ResponseFutureEncodable + /// PATCH path for async closure returning type conforming to ResponseEncodable @discardableResult public func patch( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, HBRequestContext) -> EventLoopFuture + use handler: @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .PATCH, options: options, use: handler) } -} - -extension HBRouterMethods { - func constructResponder( - options: HBRouterMethodOptions, - use closure: @escaping (HBRequest, Context) throws -> Output - ) -> HBCallbackResponder { - // generate response from request. Moved repeated code into internal function - @Sendable func _respond(request: HBRequest, context: Context) throws -> HBResponse { - return try closure(request, context).response(from: request, context: context) - } - - if options.contains(.streamBody) { - return HBCallbackResponder { request, context in - do { - let response = try _respond(request: request, context: context) - return context.success(response) - } catch { - return context.failure(error) - } - } - } else { - return HBCallbackResponder { request, context in - if case .byteBuffer = request.body { - do { - let response = try _respond(request: request, context: context) - return context.success(response) - } catch { - return context.failure(error) - } - } else { - return request.body.consumeBody( - maxSize: context.applicationContext.configuration.maxUploadSize, - on: context.eventLoop - ).flatMapThrowing { buffer in - var request = request - request.body = .byteBuffer(buffer) - return try _respond(request: request, context: context) - } - } - } - } - } func constructResponder( options: HBRouterMethodOptions, - use closure: @escaping (HBRequest, Context) -> EventLoopFuture + use closure: @escaping (HBRequest, Context) async throws -> Output ) -> HBCallbackResponder { // generate response from request. Moved repeated code into internal function - @Sendable func _respond(request: HBRequest, context: Context) -> EventLoopFuture { - let responseFuture = closure(request, context).flatMapThrowing { try $0.response(from: request, context: context) } - return responseFuture.hop(to: context.eventLoop) + @Sendable func _respond(request: HBRequest, context: Context) async throws -> HBResponse { + let output = try await closure(request, context) + return try output.response(from: request, context: context) } if options.contains(.streamBody) { return HBCallbackResponder { request, context in - return _respond(request: request, context: context) + return try await _respond(request: request, context: context) } } else { return HBCallbackResponder { request, context in if case .byteBuffer = request.body { - return _respond(request: request, context: context) + return try await _respond(request: request, context: context) } else { - return request.body.consumeBody( + let buffer = try await request.body.consumeBody( maxSize: context.applicationContext.configuration.maxUploadSize, on: context.eventLoop - ).flatMap { buffer in - var request = request - request.body = .byteBuffer(buffer) - return _respond(request: request, context: context) - } + ).get() + var request = request + request.body = .byteBuffer(buffer) + return try await _respond(request: request, context: context) } } } diff --git a/Sources/Hummingbird/Server/Application+HTTPResponder.swift b/Sources/Hummingbird/Server/Application+HTTPResponder.swift index bd7579715..0f5250481 100644 --- a/Sources/Hummingbird/Server/Application+HTTPResponder.swift +++ b/Sources/Hummingbird/Server/Application+HTTPResponder.swift @@ -40,8 +40,11 @@ extension HBApplication { logger: loggerWithRequestId(self.applicationContext.logger) ) let httpVersion = request.version + // respond to request - self.responder.respond(to: request, context: context).whenComplete { result in + context.eventLoop.makeFutureWithTask { + try await self.responder.respond(to: request, context: context) + }.whenComplete { result in switch result { case .success(let response): var response = response diff --git a/Sources/Hummingbird/Server/RequestContext.swift b/Sources/Hummingbird/Server/RequestContext.swift index b40606887..5574db83e 100644 --- a/Sources/Hummingbird/Server/RequestContext.swift +++ b/Sources/Hummingbird/Server/RequestContext.swift @@ -15,21 +15,22 @@ import Atomics import Logging import NIOCore +import NIOConcurrencyHelpers import Tracing /// Endpoint path storage public struct EndpointPath: Sendable { public init(eventLoop: EventLoop) { - self._value = .init(nil, eventLoop: eventLoop) + self._value = .init(nil) } /// Endpoint path public internal(set) var value: String? { - get { self._value.value } - nonmutating set { self._value.value = newValue } + get { self._value.withLockedValue{ $0 } } + nonmutating set { self._value.withLockedValue { $0 = newValue } } } - private let _value: NIOLoopBoundBox + private let _value: NIOLockedValueBox } /// Request context values required by Hummingbird itself. diff --git a/Sources/Hummingbird/Server/Responder.swift b/Sources/Hummingbird/Server/Responder.swift index 27a54bb1b..49131a9f6 100644 --- a/Sources/Hummingbird/Server/Responder.swift +++ b/Sources/Hummingbird/Server/Responder.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import NIOCore +import ServiceContextModule /// Protocol for object that produces a response given a request /// @@ -20,19 +21,21 @@ import NIOCore public protocol HBResponder: Sendable { associatedtype Context: HBRequestContext /// Return EventLoopFuture that will be fulfilled with response to the request supplied - func respond(to request: HBRequest, context: Context) -> EventLoopFuture + func respond(to request: HBRequest, context: Context) async throws -> HBResponse } /// Responder that calls supplied closure public struct HBCallbackResponder: HBResponder { - let callback: @Sendable (HBRequest, Context) -> EventLoopFuture + let callback: @Sendable (HBRequest, Context ) async throws -> HBResponse - public init(callback: @escaping @Sendable (HBRequest, Context) -> EventLoopFuture) { + public init(callback: @escaping @Sendable (HBRequest, Context) async throws -> HBResponse) { self.callback = callback } /// Return EventLoopFuture that will be fulfilled with response to the request supplied - public func respond(to request: HBRequest, context: Context) -> EventLoopFuture { - return self.callback(request, context) + public func respond(to request: HBRequest, context: Context) async throws -> HBResponse { + return try await ServiceContext.$current.withValue(context.serviceContext) { + try await self.callback(request, context) + } } } diff --git a/Sources/Hummingbird/Server/ServiceContext.swift b/Sources/Hummingbird/Server/ServiceContext.swift index 80facc95e..f30ad0ad7 100644 --- a/Sources/Hummingbird/Server/ServiceContext.swift +++ b/Sources/Hummingbird/Server/ServiceContext.swift @@ -26,10 +26,10 @@ extension HBTracingRequestContext { /// - serviceContext: ServiceContext to attach to request /// - operation: operation to run /// - Returns: return value of operation - public func withServiceContext(_ serviceContext: ServiceContext, _ operation: (Self) throws -> Return) rethrows -> Return { + public func withServiceContext(_ serviceContext: ServiceContext, _ operation: (Self) async throws -> Return) async rethrows -> Return { var context = self context.serviceContext = serviceContext - return try operation(context) + return try await operation(context) } /// Execute the given operation within a newly created ``Span`` @@ -52,9 +52,9 @@ extension HBTracingRequestContext { public func withSpan( _ operationName: String, ofKind kind: SpanKind = .internal, - _ operation: (Self, Span) throws -> Return - ) rethrows -> Return { - return try self.withSpan(operationName, serviceContext: self.serviceContext, ofKind: kind, operation) + _ operation: (Self, Span) async throws -> Return + ) async rethrows -> Return { + return try await self.withSpan(operationName, serviceContext: self.serviceContext, ofKind: kind, operation) } /// Execute a specific task within a newly created ``Span``. @@ -79,69 +79,17 @@ extension HBTracingRequestContext { _ operationName: String, serviceContext: ServiceContext, ofKind kind: SpanKind = .internal, - _ operation: (Self, Span) throws -> Return - ) rethrows -> Return { + _ operation: (Self, Span) async throws -> Return + ) async rethrows -> Return { let span = InstrumentationSystem.legacyTracer.startAnySpan(operationName, context: serviceContext, ofKind: kind) defer { span.end() } - return try self.withServiceContext(span.context) { request in + return try await self.withServiceContext(span.context) { request in do { - return try operation(request, span) + return try await operation(request, span) } catch { span.recordError(error) throw error } } } - - /// Execute the given operation within a newly created ``Span`` - /// - /// Calls operation with edited request that includes the serviceContext from span, and the span. Be sure to use the - /// `HBRequest` passed to the closure as that includes the serviceContext - /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - Returns: the value returned by `operation` - /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( - _ operationName: String, - ofKind kind: SpanKind = .internal, - _ operation: (Self, Span) -> EventLoopFuture - ) -> EventLoopFuture { - return self.withSpan(operationName, serviceContext: self.serviceContext, ofKind: kind, operation) - } - - /// Execute the given operation within a newly created ``Span``, - /// - /// Calls operation with edited request that includes the serviceContext, and the span. Be sure to use the - /// `HBRequest` passed to the closure as that includes the serviceContext - /// - /// DO NOT `end()` the passed in span manually. It will be ended automatically when the `operation` returns. - /// - /// - Parameters: - /// - operationName: The name of the operation being traced. This may be a handler function, database call, ... - /// - kind: The ``SpanKind`` of the ``Span`` to be created. Defaults to ``SpanKind/internal``. - /// - operation: operation to wrap in a span start/end and execute immediately - /// - Returns: the value returned by `operation` - /// - Throws: the error the `operation` has thrown (if any) - public func withSpan( - _ operationName: String, - serviceContext: ServiceContext, - ofKind kind: SpanKind = .internal, - _ operation: (Self, Span) -> EventLoopFuture - ) -> EventLoopFuture { - let span = InstrumentationSystem.legacyTracer.startAnySpan(operationName, context: serviceContext, ofKind: kind) - return self.withServiceContext(span.context) { context in - return operation(context, span) - .flatMapErrorThrowing { error in - span.recordError(error) - throw error - }.always { _ in - span.end() - } - } - } } diff --git a/Sources/Hummingbird/TemporaryELFSupport/HBResponder+ELF.swift b/Sources/Hummingbird/TemporaryELFSupport/HBResponder+ELF.swift new file mode 100644 index 000000000..76f71b921 --- /dev/null +++ b/Sources/Hummingbird/TemporaryELFSupport/HBResponder+ELF.swift @@ -0,0 +1,10 @@ +import NIOCore + +extension HBResponder { + @available(*, noasync) + public func respond(to request: HBRequest, context: Context) -> EventLoopFuture { + context.eventLoop.makeFutureWithTask { + try await self.respond(to: request, context: context) + } + } +} \ No newline at end of file diff --git a/Tests/HummingbirdFoundationTests/FilesTests.swift b/Tests/HummingbirdFoundationTests/FilesTests.swift index 947012d4a..e8f371692 100644 --- a/Tests/HummingbirdFoundationTests/FilesTests.swift +++ b/Tests/HummingbirdFoundationTests/FilesTests.swift @@ -304,10 +304,10 @@ class HummingbirdFilesTests: XCTestCase { func testWrite() async throws { let filename = "testWrite.txt" let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.put("store") { request, context -> EventLoopFuture in + router.put("store") { request, context -> HTTPResponseStatus in let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) - return fileIO.writeFile(contents: request.body, path: filename, context: context, logger: context.logger) - .map { .ok } + try await fileIO.writeFile(contents: request.body, path: filename, context: context, logger: context.logger) + return .ok } let app = HBApplication(responder: router.buildResponder()) @@ -327,10 +327,10 @@ class HummingbirdFilesTests: XCTestCase { func testWriteLargeFile() async throws { let filename = "testWriteLargeFile.txt" let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.put("store") { request, context -> EventLoopFuture in + router.put("store") { request, context -> HTTPResponseStatus in let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) - return fileIO.writeFile(contents: request.body, path: filename, context: context, logger: context.logger) - .map { .ok } + try await fileIO.writeFile(contents: request.body, path: filename, context: context, logger: context.logger).get() + return .ok } let app = HBApplication(responder: router.buildResponder()) diff --git a/Tests/HummingbirdTests/ApplicationTests.swift b/Tests/HummingbirdTests/ApplicationTests.swift index 7dad72ec0..11653ba2f 100644 --- a/Tests/HummingbirdTests/ApplicationTests.swift +++ b/Tests/HummingbirdTests/ApplicationTests.swift @@ -27,9 +27,8 @@ final class ApplicationTests: XCTestCase { func testGetRoute() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.get("/hello") { _, context -> EventLoopFuture in - let buffer = context.allocator.buffer(string: "GET: Hello") - return context.eventLoop.makeSucceededFuture(buffer) + router.get("/hello") { _, context -> ByteBuffer in + return context.allocator.buffer(string: "GET: Hello") } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -146,9 +145,9 @@ final class ApplicationTests: XCTestCase { func testQueryRoute() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.post("/query") { request, context -> EventLoopFuture in + router.post("/query") { request, context -> ByteBuffer in let buffer = context.allocator.buffer(string: request.uri.queryParameters["test"].map { String($0) } ?? "") - return context.eventLoop.makeSucceededFuture(buffer) + return context.allocator.buffer(string: request.uri.queryParameters["test"].map { String($0) } ?? "") } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -196,12 +195,11 @@ final class ApplicationTests: XCTestCase { func testEventLoopFutureArray() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.patch("array") { _, context -> EventLoopFuture<[String]> in - return context.success(["yes", "no"]) + router.patch("array") { _, context -> [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\"]") @@ -248,16 +246,16 @@ final class ApplicationTests: XCTestCase { } return HBResponse(status: .ok, headers: [:], body: .stream(RequestStreamer(stream: stream))) } - router.post("size", options: .streamBody) { request, context -> EventLoopFuture in + router.post("size", options: .streamBody) { request, context -> String in guard let stream = request.body.stream else { - return context.failure(.badRequest) + throw HBHTTPError(.badRequest) } let size = ManagedAtomic(0) - return stream.consumeAll(on: context.eventLoop) { buffer in + _ = try await stream.consumeAll(on: context.eventLoop) { buffer in size.wrappingIncrement(by: buffer.readableBytes, ordering: .relaxed) return context.success(()) - } - .map { _ in size.load(ordering: .relaxed).description } + }.get() + return size.load(ordering: .relaxed).description } let app = HBApplication(responder: router.buildResponder()) @@ -362,8 +360,8 @@ final class ApplicationTests: XCTestCase { let router = HBRouterBuilder(context: HBTestRouterContext.self) router .group("/echo-body") - .post { request, context -> EventLoopFuture in - return context.success(request.body.buffer) + .post { request, context -> ByteBuffer? in + return request.body.buffer } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -455,12 +453,10 @@ final class ApplicationTests: XCTestCase { func testTypedResponseFuture() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) router.delete("/hello") { _, context in - return context.success( - HBEditedResponse( - status: .imATeapot, - headers: ["test": "value", "content-type": "application/json"], - response: "Hello" - ) + HBEditedResponse( + status: .imATeapot, + headers: ["test": "value", "content-type": "application/json"], + response: "Hello" ) } let app = HBApplication(responder: router.buildResponder()) diff --git a/Tests/HummingbirdTests/AsyncAwaitTests.swift b/Tests/HummingbirdTests/AsyncAwaitTests.swift index 61888f6f6..6696dd7f0 100644 --- a/Tests/HummingbirdTests/AsyncAwaitTests.swift +++ b/Tests/HummingbirdTests/AsyncAwaitTests.swift @@ -84,7 +84,7 @@ final class AsyncAwaitTests: XCTestCase { } func testAsyncRouteHandler() async throws { - struct AsyncTest: HBAsyncRouteHandler { + struct AsyncTest: HBRouteHandler { let name: String init(from request: HBRequest, context: HBRequestContext) throws { self.name = try context.parameters.require("name") diff --git a/Tests/HummingbirdTests/HandlerTests.swift b/Tests/HummingbirdTests/HandlerTests.swift index ed2f97b20..79fb9723c 100644 --- a/Tests/HummingbirdTests/HandlerTests.swift +++ b/Tests/HummingbirdTests/HandlerTests.swift @@ -168,8 +168,8 @@ final class HandlerTests: XCTestCase { func testDecodeFutureResponse() async throws { struct DecodeTest: HBRequestDecodable { let name: String - func handle(request: HBRequest, context: HBRequestContext) -> EventLoopFuture { - return context.success("Hello \(self.name)") + func handle(request: HBRequest, context: HBRequestContext) async throws -> String { + "Hello \(self.name)" } } let router = HBRouterBuilder(context: HBTestRouterContext.self) diff --git a/Tests/HummingbirdTests/MiddlewareTests.swift b/Tests/HummingbirdTests/MiddlewareTests.swift index f321ef043..2c825bf07 100644 --- a/Tests/HummingbirdTests/MiddlewareTests.swift +++ b/Tests/HummingbirdTests/MiddlewareTests.swift @@ -93,11 +93,12 @@ final class MiddlewareTests: XCTestCase { func testMiddlewareRunWhenNoRouteFound() async throws { struct TestMiddleware: HBMiddleware { func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { - return next.respond(to: request, context: context).flatMapError { error in - guard let httpError = error as? HBHTTPError, httpError.status == .notFound else { - return context.failure(error) + return context.eventLoop.makeFutureWithTask { + do { + return try await next.respond(to: request, context: context) + } catch let error as HBHTTPError where error.status == .notFound { + throw HBHTTPError(.notFound, message: "Edited error") } - return context.failure(.notFound, message: "Edited error") } } } @@ -197,8 +198,8 @@ final class MiddlewareTests: XCTestCase { func testRouteLoggingMiddleware() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) router.middlewares.add(HBLogRequestsMiddleware(.debug)) - router.put("/hello") { _, context -> EventLoopFuture in - return context.failure(.badRequest) + router.put("/hello") { _, context -> String in + throw HBHTTPError(.badRequest) } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in diff --git a/Tests/HummingbirdTests/PersistTests.swift b/Tests/HummingbirdTests/PersistTests.swift index 9bdcdc796..81c0afcf7 100644 --- a/Tests/HummingbirdTests/PersistTests.swift +++ b/Tests/HummingbirdTests/PersistTests.swift @@ -23,27 +23,27 @@ final class PersistTests: XCTestCase { let router = HBRouterBuilder(context: HBTestRouterContext.self) let persist = HBMemoryPersistDriver() - router.put("/persist/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - guard let buffer = request.body.buffer else { return context.failure(.badRequest) } - return persist.set(key: tag, value: String(buffer: buffer), request: request) - .map { _ in .ok } - } - router.put("/persist/:tag/:time") { request, context -> EventLoopFuture in - guard let time = context.parameters.get("time", as: Int.self) else { return context.failure(.badRequest) } - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - guard let buffer = request.body.buffer else { return context.failure(.badRequest) } - return persist.set(key: tag, value: String(buffer: buffer), expires: .seconds(numericCast(time)), request: request) - .map { _ in .ok } - } - router.get("/persist/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag", as: String.self) else { return context.failure(.badRequest) } - return persist.get(key: tag, as: String.self, request: request) - } - router.delete("/persist/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag", as: String.self) else { return context.failure(.badRequest) } - return persist.remove(key: tag, request: request) - .map { _ in .noContent } + router.put("/persist/:tag") { request, context -> HTTPResponseStatus in + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + try await persist.set(key: tag, value: String(buffer: buffer), request: request) + return .ok + } + router.put("/persist/:tag/:time") { request, context -> HTTPResponseStatus in + guard let time = context.parameters.get("time", as: Int.self) else { throw HBHTTPError(.badRequest) } + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + try await persist.set(key: tag, value: String(buffer: buffer), expires: .seconds(numericCast(time)), request: request).get() + return .ok + } + router.get("/persist/:tag") { request, context -> String? in + guard let tag = context.parameters.get("tag", as: String.self) else { throw HBHTTPError(.badRequest) } + return try await persist.get(key: tag, as: String.self, request: request) + } + router.delete("/persist/:tag") { request, context -> HTTPResponseStatus in + guard let tag = context.parameters.get("tag", as: String.self) else { throw HBHTTPError(.badRequest) } + try await persist.remove(key: tag, request: request).get() + return .noContent } return (router, persist) } @@ -63,11 +63,12 @@ final class PersistTests: XCTestCase { func testCreateGet() async throws { let (router, persist) = try createRouter() - router.put("/create/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - guard let buffer = request.body.buffer else { return context.failure(.badRequest) } - return persist.create(key: tag, value: String(buffer: buffer), request: request) - .map { _ in .ok } + + router.put("/create/:tag") { request, context -> HTTPResponseStatus in + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + try await persist.create(key: tag, value: String(buffer: buffer), request: request).get() + return .ok } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -82,15 +83,15 @@ final class PersistTests: XCTestCase { func testDoubleCreateFail() async throws { let (router, persist) = try createRouter() - router.put("/create/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - guard let buffer = request.body.buffer else { return context.failure(.badRequest) } - return persist.create(key: tag, value: String(buffer: buffer), request: request) + router.put("/create/:tag") { request, context -> HTTPResponseStatus in + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + try await persist.create(key: tag, value: String(buffer: buffer), request: request) .flatMapErrorThrowing { error in if let error = error as? HBPersistError, error == .duplicate { throw HBHTTPError(.conflict) } throw error - } - .map { _ in .ok } + }.get() + return .ok } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -147,15 +148,15 @@ final class PersistTests: XCTestCase { let buffer: String } let (router, persist) = try createRouter() - router.put("/codable/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - guard let buffer = request.body.buffer else { return context.failure(.badRequest) } - return persist.set(key: tag, value: TestCodable(buffer: String(buffer: buffer)), request: request) - .map { _ in .ok } + router.put("/codable/:tag") { request, context -> HTTPResponseStatus in + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + try await persist.set(key: tag, value: TestCodable(buffer: String(buffer: buffer)), request: request).get() + return .ok } - router.get("/codable/:tag") { request, context -> EventLoopFuture in - guard let tag = context.parameters.get("tag") else { return context.failure(.badRequest) } - return persist.get(key: tag, as: TestCodable.self, request: request).map { $0.map(\.buffer) } + router.get("/codable/:tag") { request, context -> String? in + guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } + return try await persist.get(key: tag, as: TestCodable.self, request: request).get()?.buffer } let app = HBApplication(responder: router.buildResponder()) @@ -174,7 +175,6 @@ final class PersistTests: XCTestCase { let (router, _) = try createRouter() let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in - let tag = UUID().uuidString try await client.XCTExecute(uri: "/persist/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest1")) { _ in } try await client.XCTExecute(uri: "/persist/\(tag)", method: .DELETE) { _ in } diff --git a/Tests/HummingbirdTests/RouterTests.swift b/Tests/HummingbirdTests/RouterTests.swift index e9b051e40..575f47619 100644 --- a/Tests/HummingbirdTests/RouterTests.swift +++ b/Tests/HummingbirdTests/RouterTests.swift @@ -221,7 +221,7 @@ final class RouterTests: XCTestCase { .add(middleware: TestMiddleware()) .group("/group") .get { _, context in - return context.success("hello") + return "hello" } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -248,12 +248,12 @@ final class RouterTests: XCTestCase { .group("/test") .add(middleware: TestGroupMiddleware(output: "route1")) .get { _, context in - return context.success(context.string) + return context.string } .group("/group") .add(middleware: TestGroupMiddleware(output: "route2")) .get { _, context in - return context.success(context.string) + return context.string } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in diff --git a/Tests/HummingbirdTests/TracingTests.swift b/Tests/HummingbirdTests/TracingTests.swift index bfcd27d3a..01d575546 100644 --- a/Tests/HummingbirdTests/TracingTests.swift +++ b/Tests/HummingbirdTests/TracingTests.swift @@ -330,7 +330,7 @@ final class TracingTests: XCTestCase { router.get("/") { _, context -> HTTPResponseStatus in var serviceContext = context.serviceContext serviceContext.testID = "test" - return context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .client) { _, span in + return try await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .client) { _, span in span.attributes["test-attribute"] = 42 return .ok } @@ -361,10 +361,13 @@ final class TracingTests: XCTestCase { struct SpanMiddleware: HBMiddleware { public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { - var serviceContext = context.serviceContext - serviceContext.testID = "testMiddleware" - return context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .server) { context, _ in - next.respond(to: request, context: context) + return context.eventLoop.makeFutureWithTask { + var serviceContext = context.serviceContext + serviceContext.testID = "testMiddleware" + + return try await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .server) { context, _ in + try await next.respond(to: request, context: context) + } } } } @@ -378,8 +381,8 @@ final class TracingTests: XCTestCase { let router = HBRouterBuilder(context: HBTestRouterContext.self) router.middlewares.add(SpanMiddleware()) router.middlewares.add(HBTracingMiddleware()) - router.get("/") { _, context -> EventLoopFuture in - return context.eventLoop.scheduleTask(in: .milliseconds(2)) { return .ok }.futureResult + router.get("/") { _, context -> HTTPResponseStatus in + return try await context.eventLoop.scheduleTask(in: .milliseconds(2)) { return .ok }.futureResult.get() } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in @@ -459,7 +462,7 @@ extension TracingTests { router.middlewares.add(AsyncSpanMiddleware()) router.get("/") { _, context -> HTTPResponseStatus in try await Task.sleep(nanoseconds: 1000) - return context.withSpan("testing", ofKind: .server) { _, _ in + return try await context.withSpan("testing", ofKind: .server) { _, _ in return .ok } } diff --git a/documentation/Encoding and Decoding.md b/documentation/Encoding and Decoding.md index 7ad8f59c1..cc13fd146 100644 --- a/documentation/Encoding and Decoding.md +++ b/documentation/Encoding and Decoding.md @@ -49,7 +49,7 @@ struct User: Decodable { app.router.post("user") { request -> EventLoopFuture in // decode user from request guard let user = try? request.decode(as: User.self) else { - return context.failure(.badRequest) + throw HBHTTPError(.badRequest) } // create user and if ok return `.ok` status return createUser(user, on: context.eventLoop) diff --git a/documentation/Error Handling.md b/documentation/Error Handling.md index 2b21d3dca..254e0e73a 100644 --- a/documentation/Error Handling.md +++ b/documentation/Error Handling.md @@ -21,7 +21,7 @@ In the situation where you have a route that returns an `EventLoopFuture` you ar ```swift app.get("user") { request -> EventLoopFuture in guard let userId = request.uri.queryParameters.get("id", as: Int.self) else { - return context.failure(.badRequest, message: "Invalid user id") + throw HBHTTPError(.badRequest, message: "Invalid user id") } ... } diff --git a/documentation/Router.md b/documentation/Router.md index 3fe923f73..04d1e2a97 100644 --- a/documentation/Router.md +++ b/documentation/Router.md @@ -108,7 +108,7 @@ By default Hummingbird will collate the contents of your request body into one B ```swift application.router.post("size", options: .streamBody) { request -> EventLoopFuture in guard let stream = request.body.stream else { - return context.failure(.badRequest) + throw HBHTTPError(.badRequest) } var size = 0 return stream.consumeAll(on: context.eventLoop) { buffer in From 5bb58c7a432cd261dcf54ae2c27e5b7debc5abaf Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 29 Oct 2023 19:17:52 +0100 Subject: [PATCH 2/8] Remove some obsolete docs --- Sources/Hummingbird/Server/Responder.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Hummingbird/Server/Responder.swift b/Sources/Hummingbird/Server/Responder.swift index 49131a9f6..7f12f76c7 100644 --- a/Sources/Hummingbird/Server/Responder.swift +++ b/Sources/Hummingbird/Server/Responder.swift @@ -32,7 +32,6 @@ public struct HBCallbackResponder: HBResponder { self.callback = callback } - /// Return EventLoopFuture that will be fulfilled with response to the request supplied public func respond(to request: HBRequest, context: Context) async throws -> HBResponse { return try await ServiceContext.$current.withValue(context.serviceContext) { try await self.callback(request, context) From 68302d15c8ba78ba82a2d22099c15f22cca62c61 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 09:52:32 +0100 Subject: [PATCH 3/8] Simplify the middleware setup converting from async/await to ELF --- .../Middleware/CORSMiddleware.swift | 74 ++++++----- .../Middleware/MetricsMiddleware.swift | 68 +++++----- .../Middleware/TracingMiddleware.swift | 120 +++++++++--------- Tests/HummingbirdTests/TracingTests.swift | 18 ++- 4 files changed, 147 insertions(+), 133 deletions(-) diff --git a/Sources/Hummingbird/Middleware/CORSMiddleware.swift b/Sources/Hummingbird/Middleware/CORSMiddleware.swift index 918643119..0c8a2386e 100644 --- a/Sources/Hummingbird/Middleware/CORSMiddleware.swift +++ b/Sources/Hummingbird/Middleware/CORSMiddleware.swift @@ -85,44 +85,48 @@ public struct HBCORSMiddleware: HBMiddleware { /// apply CORS middleware public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { context.eventLoop.makeFutureWithTask { - // if no origin header then don't apply CORS - guard request.headers["origin"].first != nil else { - return try await next.respond(to: request, context: context) - } + try await self.apply(to: request, context: context, next: next) + } + } + + public func apply(to request: HBRequest, context: Context, next: any HBResponder) async throws -> HBResponse { + // if no origin header then don't apply CORS + guard request.headers["origin"].first != nil else { + return try await next.respond(to: request, context: context) + } - if request.method == .OPTIONS { - // if request is OPTIONS then return CORS headers and skip the rest of the middleware chain - var headers: HTTPHeaders = [ - "access-control-allow-origin": allowOrigin.value(for: request) ?? "", - ] - headers.add(name: "access-control-allow-headers", value: self.allowHeaders) - headers.add(name: "access-control-allow-methods", value: self.allowMethods) - if self.allowCredentials { - headers.add(name: "access-control-allow-credentials", value: "true") - } - if let maxAge = self.maxAge { - headers.add(name: "access-control-max-age", value: maxAge) - } - if let exposedHeaders = self.exposedHeaders { - headers.add(name: "access-control-expose-headers", value: exposedHeaders) - } - if case .originBased = self.allowOrigin { - headers.add(name: "vary", value: "Origin") - } + if request.method == .OPTIONS { + // if request is OPTIONS then return CORS headers and skip the rest of the middleware chain + var headers: HTTPHeaders = [ + "access-control-allow-origin": allowOrigin.value(for: request) ?? "", + ] + headers.add(name: "access-control-allow-headers", value: self.allowHeaders) + headers.add(name: "access-control-allow-methods", value: self.allowMethods) + if self.allowCredentials { + headers.add(name: "access-control-allow-credentials", value: "true") + } + if let maxAge = self.maxAge { + headers.add(name: "access-control-max-age", value: maxAge) + } + if let exposedHeaders = self.exposedHeaders { + headers.add(name: "access-control-expose-headers", value: exposedHeaders) + } + if case .originBased = self.allowOrigin { + headers.add(name: "vary", value: "Origin") + } - return HBResponse(status: .noContent, headers: headers, body: .empty) - } else { - // if not OPTIONS then run rest of middleware chain and add origin value at the end - var response = try await next.respond(to: request, context: context) - response.headers.add(name: "access-control-allow-origin", value: self.allowOrigin.value(for: request) ?? "") - if self.allowCredentials { - response.headers.add(name: "access-control-allow-credentials", value: "true") - } - if case .originBased = self.allowOrigin { - response.headers.add(name: "vary", value: "Origin") - } - return response + return HBResponse(status: .noContent, headers: headers, body: .empty) + } else { + // if not OPTIONS then run rest of middleware chain and add origin value at the end + var response = try await next.respond(to: request, context: context) + response.headers.add(name: "access-control-allow-origin", value: self.allowOrigin.value(for: request) ?? "") + if self.allowCredentials { + response.headers.add(name: "access-control-allow-credentials", value: "true") + } + if case .originBased = self.allowOrigin { + response.headers.add(name: "vary", value: "Origin") } + return response } } } diff --git a/Sources/Hummingbird/Middleware/MetricsMiddleware.swift b/Sources/Hummingbird/Middleware/MetricsMiddleware.swift index bb6bf95d2..3b2d58c16 100644 --- a/Sources/Hummingbird/Middleware/MetricsMiddleware.swift +++ b/Sources/Hummingbird/Middleware/MetricsMiddleware.swift @@ -23,45 +23,47 @@ public struct HBMetricsMiddleware: HBMiddleware { public init() {} public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { + context.eventLoop.makeFutureWithTask { + try await apply(to: request, context: context, next: next) + } + } + + public func apply(to request: HBRequest, context: Context, next: any HBResponder) async throws -> HBResponse { let startTime = DispatchTime.now().uptimeNanoseconds - let promise = context.eventLoop.makePromise(of: HBResponse.self) - promise.completeWithTask { - do { - let response = try await next.respond(to: request, context: context) - // need to create dimensions once request has been responded to ensure - // we have the correct endpoint path - let dimensions: [(String, String)] = [ - ("hb_uri", context.endpointPath ?? request.uri.path), + do { + let response = try await next.respond(to: request, context: context) + // need to create dimensions once request has been responded to ensure + // we have the correct endpoint path + let dimensions: [(String, String)] = [ + ("hb_uri", context.endpointPath ?? request.uri.path), + ("hb_method", request.method.rawValue), + ] + Counter(label: "hb_requests", dimensions: dimensions).increment() + Metrics.Timer( + label: "hb_request_duration", + dimensions: dimensions, + preferredDisplayUnit: .seconds + ).recordNanoseconds(DispatchTime.now().uptimeNanoseconds - startTime) + return response + } catch { + // need to create dimensions once request has been responded to ensure + // we have the correct endpoint path + let dimensions: [(String, String)] + // Don't record uri in 404 errors, to avoid spamming of metrics + if let endpointPath = context.endpointPath { + dimensions = [ + ("hb_uri", endpointPath), ("hb_method", request.method.rawValue), ] Counter(label: "hb_requests", dimensions: dimensions).increment() - Metrics.Timer( - label: "hb_request_duration", - dimensions: dimensions, - preferredDisplayUnit: .seconds - ).recordNanoseconds(DispatchTime.now().uptimeNanoseconds - startTime) - return response - } catch { - // need to create dimensions once request has been responded to ensure - // we have the correct endpoint path - let dimensions: [(String, String)] - // Don't record uri in 404 errors, to avoid spamming of metrics - if let endpointPath = context.endpointPath { - dimensions = [ - ("hb_uri", endpointPath), - ("hb_method", request.method.rawValue), - ] - Counter(label: "hb_requests", dimensions: dimensions).increment() - } else { - dimensions = [ - ("hb_method", request.method.rawValue), - ] - } - Counter(label: "hb_errors", dimensions: dimensions).increment() - throw error + } else { + dimensions = [ + ("hb_method", request.method.rawValue), + ] } + Counter(label: "hb_errors", dimensions: dimensions).increment() + throw error } - return promise.futureResult } } diff --git a/Sources/Hummingbird/Middleware/TracingMiddleware.swift b/Sources/Hummingbird/Middleware/TracingMiddleware.swift index c0a2ab75c..4889ea80e 100644 --- a/Sources/Hummingbird/Middleware/TracingMiddleware.swift +++ b/Sources/Hummingbird/Middleware/TracingMiddleware.swift @@ -39,72 +39,76 @@ public struct HBTracingMiddleware) -> EventLoopFuture { return context.eventLoop.makeFutureWithTask { - var serviceContext = context.serviceContext - InstrumentationSystem.instrument.extract(request.headers, into: &serviceContext, using: HTTPHeadersExtractor()) + try await apply(to: request, context: context, next: next) + } + } - let operationName: String = { - guard let endpointPath = context.endpointPath else { - return "HTTP \(request.method.rawValue) route not found" - } - return endpointPath - }() + public func apply(to request: HBRequest, context: Context, next: any HBResponder) async throws -> HBResponse { + var serviceContext = context.serviceContext + InstrumentationSystem.instrument.extract(request.headers, into: &serviceContext, using: HTTPHeadersExtractor()) - return try await context.withSpan(operationName, serviceContext: serviceContext, ofKind: .server) { context, span in - span.updateAttributes { attributes in - attributes["http.method"] = request.method.rawValue - attributes["http.target"] = request.uri.path - attributes["http.flavor"] = "\(request.version.major).\(request.version.minor)" - attributes["http.scheme"] = request.uri.scheme?.rawValue - attributes["http.user_agent"] = request.headers.first(name: "user-agent") - attributes["http.request_content_length"] = request.headers["content-length"].first.map { Int($0) } ?? nil - - attributes["net.host.name"] = context.applicationContext.configuration.address.host - attributes["net.host.port"] = context.applicationContext.configuration.address.port - - if let remoteAddress = context.remoteAddress { - attributes["net.sock.peer.port"] = remoteAddress.port - - switch remoteAddress.protocol { - case .inet: - attributes["net.sock.peer.addr"] = remoteAddress.ipAddress - case .inet6: - attributes["net.sock.family"] = "inet6" - attributes["net.sock.peer.addr"] = remoteAddress.ipAddress - case .unix: - attributes["net.sock.family"] = "unix" - attributes["net.sock.peer.addr"] = remoteAddress.pathname - default: - break - } + let operationName: String = { + guard let endpointPath = context.endpointPath else { + return "HTTP \(request.method.rawValue) route not found" + } + return endpointPath + }() + + return try await context.withSpan(operationName, serviceContext: serviceContext, ofKind: .server) { context, span in + span.updateAttributes { attributes in + attributes["http.method"] = request.method.rawValue + attributes["http.target"] = request.uri.path + attributes["http.flavor"] = "\(request.version.major).\(request.version.minor)" + attributes["http.scheme"] = request.uri.scheme?.rawValue + attributes["http.user_agent"] = request.headers.first(name: "user-agent") + attributes["http.request_content_length"] = request.headers["content-length"].first.map { Int($0) } ?? nil + + attributes["net.host.name"] = context.applicationContext.configuration.address.host + attributes["net.host.port"] = context.applicationContext.configuration.address.port + + if let remoteAddress = context.remoteAddress { + attributes["net.sock.peer.port"] = remoteAddress.port + + switch remoteAddress.protocol { + case .inet: + attributes["net.sock.peer.addr"] = remoteAddress.ipAddress + case .inet6: + attributes["net.sock.family"] = "inet6" + attributes["net.sock.peer.addr"] = remoteAddress.ipAddress + case .unix: + attributes["net.sock.family"] = "unix" + attributes["net.sock.peer.addr"] = remoteAddress.pathname + default: + break } - attributes = self.recordHeaders(request.headers, toSpanAttributes: attributes, withPrefix: "http.request.header.") } + attributes = self.recordHeaders(request.headers, toSpanAttributes: attributes, withPrefix: "http.request.header.") + } - do { - let response = try await next.respond(to: request, context: context) - span.updateAttributes { attributes in - attributes = self.recordHeaders(response.headers, toSpanAttributes: attributes, withPrefix: "http.response.header.") - - attributes["http.status_code"] = Int(response.status.code) - switch response.body { - case .byteBuffer(let buffer): - attributes["http.response_content_length"] = buffer.readableBytes - case .stream: - attributes["http.response_content_length"] = response.headers["content-length"].first.map { Int($0) } ?? nil - case .empty: - break - } - } - return response - } catch let error as HBHTTPResponseError { - span.attributes["http.status_code"] = Int(error.status.code) - - if 500..<600 ~= error.status.code { - span.setStatus(.init(code: .error)) + do { + let response = try await next.respond(to: request, context: context) + span.updateAttributes { attributes in + attributes = self.recordHeaders(response.headers, toSpanAttributes: attributes, withPrefix: "http.response.header.") + + attributes["http.status_code"] = Int(response.status.code) + switch response.body { + case .byteBuffer(let buffer): + attributes["http.response_content_length"] = buffer.readableBytes + case .stream: + attributes["http.response_content_length"] = response.headers["content-length"].first.map { Int($0) } ?? nil + case .empty: + break } + } + return response + } catch let error as HBHTTPResponseError { + span.attributes["http.status_code"] = Int(error.status.code) - throw error + if 500..<600 ~= error.status.code { + span.setStatus(.init(code: .error)) } + + throw error } } } diff --git a/Tests/HummingbirdTests/TracingTests.swift b/Tests/HummingbirdTests/TracingTests.swift index 01d575546..2e5e12043 100644 --- a/Tests/HummingbirdTests/TracingTests.swift +++ b/Tests/HummingbirdTests/TracingTests.swift @@ -330,7 +330,7 @@ final class TracingTests: XCTestCase { router.get("/") { _, context -> HTTPResponseStatus in var serviceContext = context.serviceContext serviceContext.testID = "test" - return try await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .client) { _, span in + return await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .client) { _, span in span.attributes["test-attribute"] = 42 return .ok } @@ -362,12 +362,16 @@ final class TracingTests: XCTestCase { struct SpanMiddleware: HBMiddleware { public func apply(to request: HBRequest, context: Context, next: any HBResponder) -> EventLoopFuture { return context.eventLoop.makeFutureWithTask { - var serviceContext = context.serviceContext - serviceContext.testID = "testMiddleware" + try await apply(to: request, context: context, next: next) + } + } + + public func apply(to request: HBRequest, context: Context, next: any HBResponder) async throws -> HBResponse { + var serviceContext = context.serviceContext + serviceContext.testID = "testMiddleware" - return try await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .server) { context, _ in - try await next.respond(to: request, context: context) - } + return try await context.withSpan("TestSpan", serviceContext: serviceContext, ofKind: .server) { context, _ in + try await next.respond(to: request, context: context) } } } @@ -462,7 +466,7 @@ extension TracingTests { router.middlewares.add(AsyncSpanMiddleware()) router.get("/") { _, context -> HTTPResponseStatus in try await Task.sleep(nanoseconds: 1000) - return try await context.withSpan("testing", ofKind: .server) { _, _ in + return await context.withSpan("testing", ofKind: .server) { _, _ in return .ok } } From af23d2af8c7bd113c03e6a4e9346b5be6b46cb5b Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 09:59:00 +0100 Subject: [PATCH 4/8] Update with Adam's feedback --- .../Hummingbird/Router/RouterBuilder.swift | 2 +- .../Hummingbird/Router/RouterMethods.swift | 16 +- .../FileTests+async.swift | 77 ------- .../FilesTests.swift | 21 ++ .../HummingbirdTests/PersistTests+async.swift | 207 ------------------ Tests/HummingbirdTests/PersistTests.swift | 30 ++- Tests/HummingbirdTests/TracingTests.swift | 3 +- 7 files changed, 51 insertions(+), 305 deletions(-) delete mode 100644 Tests/HummingbirdFoundationTests/FileTests+async.swift delete mode 100644 Tests/HummingbirdTests/PersistTests+async.swift diff --git a/Sources/Hummingbird/Router/RouterBuilder.swift b/Sources/Hummingbird/Router/RouterBuilder.swift index cb01b9c09..41967c84f 100644 --- a/Sources/Hummingbird/Router/RouterBuilder.swift +++ b/Sources/Hummingbird/Router/RouterBuilder.swift @@ -93,7 +93,7 @@ public final class HBRouterBuilder: HBRouterMethods { /// Responder that return a not found error struct NotFoundResponder: HBResponder { - func respond(to request: HBRequest, context: Context) async throws -> HBResponse { + func respond(to request: HBRequest, context: Context) throws -> HBResponse { throw HBHTTPError(.notFound) } } diff --git a/Sources/Hummingbird/Router/RouterMethods.swift b/Sources/Hummingbird/Router/RouterMethods.swift index 5465d19ed..96aea263d 100644 --- a/Sources/Hummingbird/Router/RouterMethods.swift +++ b/Sources/Hummingbird/Router/RouterMethods.swift @@ -36,7 +36,7 @@ public protocol HBRouterMethods { _ path: String, method: HTTPMethod, options: HBRouterMethodOptions, - use: @escaping (HBRequest, Context) async throws -> Output + use: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self /// add group @@ -48,7 +48,7 @@ extension HBRouterMethods { @discardableResult public func get( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .GET, options: options, use: handler) } @@ -57,7 +57,7 @@ extension HBRouterMethods { @discardableResult public func put( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .PUT, options: options, use: handler) } @@ -66,7 +66,7 @@ extension HBRouterMethods { @discardableResult public func delete( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .DELETE, options: options, use: handler) } @@ -75,7 +75,7 @@ extension HBRouterMethods { @discardableResult public func head( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .HEAD, options: options, use: handler) } @@ -84,7 +84,7 @@ extension HBRouterMethods { @discardableResult public func post( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .POST, options: options, use: handler) } @@ -93,14 +93,14 @@ extension HBRouterMethods { @discardableResult public func patch( _ path: String = "", options: HBRouterMethodOptions = [], - use handler: @escaping (HBRequest, Context) async throws -> Output + use handler: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> Self { return on(path, method: .PATCH, options: options, use: handler) } func constructResponder( options: HBRouterMethodOptions, - use closure: @escaping (HBRequest, Context) async throws -> Output + use closure: @Sendable @escaping (HBRequest, Context) async throws -> Output ) -> HBCallbackResponder { // generate response from request. Moved repeated code into internal function @Sendable func _respond(request: HBRequest, context: Context) async throws -> HBResponse { diff --git a/Tests/HummingbirdFoundationTests/FileTests+async.swift b/Tests/HummingbirdFoundationTests/FileTests+async.swift deleted file mode 100644 index 60338ff71..000000000 --- a/Tests/HummingbirdFoundationTests/FileTests+async.swift +++ /dev/null @@ -1,77 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation -import Hummingbird -import HummingbirdFoundation -import HummingbirdXCT -import XCTest - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -class HummingbirdAsyncFilesTests: XCTestCase { - func randomBuffer(size: Int) -> ByteBuffer { - var data = [UInt8](repeating: 0, count: size) - data = data.map { _ in UInt8.random(in: 0...255) } - return ByteBufferAllocator().buffer(bytes: data) - } - - func testRead() async throws { - let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.get("test.jpg") { _, context -> HBResponse in - let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) - let body = try await fileIO.loadFile(path: "test.jpg", context: context, logger: context.logger) - return .init(status: .ok, headers: [:], body: body) - } - let app = HBApplication(responder: router.buildResponder()) - let buffer = self.randomBuffer(size: 320_003) - let data = Data(buffer: buffer) - let fileURL = URL(fileURLWithPath: "test.jpg") - XCTAssertNoThrow(try data.write(to: fileURL)) - defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } - - try await app.test(.router) { client in - try await client.XCTExecute(uri: "/test.jpg", method: .GET) { response in - XCTAssertEqual(response.body, buffer) - } - } - } - - func testWrite() async throws { - let filename = "testWrite.txt" - let router = HBRouterBuilder(context: HBTestRouterContext.self) - router.put("store") { request, context -> HTTPResponseStatus in - let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) - try await fileIO.writeFile( - contents: request.body, - path: filename, - context: context, - logger: context.logger - ) - return .ok - } - let app = HBApplication(responder: router.buildResponder()) - - try await app.test(.router) { client in - let buffer = ByteBufferAllocator().buffer(string: "This is a test") - try await client.XCTExecute(uri: "/store", method: .PUT, body: buffer) { response in - XCTAssertEqual(response.status, .ok) - } - - let fileURL = URL(fileURLWithPath: filename) - let data = try Data(contentsOf: fileURL) - defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } - XCTAssertEqual(String(decoding: data, as: Unicode.UTF8.self), "This is a test") - } - } -} diff --git a/Tests/HummingbirdFoundationTests/FilesTests.swift b/Tests/HummingbirdFoundationTests/FilesTests.swift index e8f371692..3ff65317d 100644 --- a/Tests/HummingbirdFoundationTests/FilesTests.swift +++ b/Tests/HummingbirdFoundationTests/FilesTests.swift @@ -53,6 +53,27 @@ class HummingbirdFilesTests: XCTestCase { } } + func testRead() async throws { + let app = HBApplicationBuilder(requestContext: HBTestRouterContext.self) + app.router.get("test.jpg") { _, context -> HBResponse in + let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) + let body = try await fileIO.loadFile(path: "test.jpg", context: context, logger: context.logger) + return .init(status: .ok, headers: [:], body: body) + } + let buffer = self.randomBuffer(size: 320_003) + let data = Data(buffer: buffer) + let fileURL = URL(fileURLWithPath: "test.jpg") + XCTAssertNoThrow(try data.write(to: fileURL)) + defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } + + try await app.buildAndTest(.router) { client in + try await client.XCTExecute(uri: "/test.jpg", method: .GET) { response in + XCTAssertEqual(response.body, buffer) + } + } + } + + func testReadLargeFile() async throws { let router = HBRouterBuilder(context: HBTestRouterContext.self) router.middlewares.add(HBFileMiddleware(".")) diff --git a/Tests/HummingbirdTests/PersistTests+async.swift b/Tests/HummingbirdTests/PersistTests+async.swift deleted file mode 100644 index a79d55270..000000000 --- a/Tests/HummingbirdTests/PersistTests+async.swift +++ /dev/null @@ -1,207 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Hummingbird -import HummingbirdXCT -import XCTest - -final class AsyncPersistTests: XCTestCase { - func createRouter() throws -> (HBRouterBuilder, HBPersistDriver) { - let router = HBRouterBuilder(context: HBTestRouterContext.self) - let persist: HBPersistDriver = HBMemoryPersistDriver() - - router.put("/persist/:tag") { request, context -> HTTPResponseStatus in - guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - let tag = try context.parameters.require("tag") - try await persist.set(key: tag, value: String(buffer: buffer), request: request) - return .ok - } - router.put("/persist/:tag/:time") { request, context -> HTTPResponseStatus in - guard let time = context.parameters.get("time", as: Int.self) else { throw HBHTTPError(.badRequest) } - guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - let tag = try context.parameters.require("tag") - try await persist.set(key: tag, value: String(buffer: buffer), expires: .seconds(numericCast(time)), request: request) - return .ok - } - router.get("/persist/:tag") { request, context -> String? in - let tag = try context.parameters.require("tag") - return try await persist.get(key: tag, as: String.self, request: request) - } - router.delete("/persist/:tag") { request, context -> HTTPResponseStatus in - let tag = try context.parameters.require("tag") - try await persist.remove(key: tag, request: request) - return .noContent - } - return (router, persist) - } - - func testSetGet() async throws { - let (router, _) = try createRouter() - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/persist/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "Persist")) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "Persist") - } - } - } - - func testCreateGet() async throws { - let (router, persist) = try createRouter() - router.put("/create/:tag") { request, context -> HTTPResponseStatus in - guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - let tag = try context.parameters.require("tag") - try await persist.create(key: tag, value: String(buffer: buffer), request: request) - return .ok - } - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/create/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "Persist")) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "Persist") - } - } - } - - func testDoubleCreateFail() async throws { - let (router, persist) = try createRouter() - router.put("/create/:tag") { request, context -> HTTPResponseStatus in - guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - let tag = try context.parameters.require("tag") - do { - try await persist.create(key: tag, value: String(buffer: buffer), request: request) - } catch let error as HBPersistError where error == .duplicate { - throw HBHTTPError(.conflict) - } - return .ok - } - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/create/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "Persist")) { response in - XCTAssertEqual(response.status, .ok) - } - try await client.XCTExecute(uri: "/create/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "Persist")) { response in - XCTAssertEqual(response.status, .conflict) - } - } - } - - func testSetTwice() async throws { - let (router, _) = try createRouter() - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/persist/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "test1")) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "test2")) { response in - XCTAssertEqual(response.status, .ok) - } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "test2") - } - } - } - - func testExpires() async throws { - let (router, _) = try createRouter() - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - - let tag1 = UUID().uuidString - let tag2 = UUID().uuidString - - try await client.XCTExecute(uri: "/persist/\(tag1)/0", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest1")) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag2)/10", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest2")) { _ in } - try await Task.sleep(nanoseconds: 1_000_000_000) - try await client.XCTExecute(uri: "/persist/\(tag1)", method: .GET) { response in - XCTAssertEqual(response.status, .noContent) - } - try await client.XCTExecute(uri: "/persist/\(tag2)", method: .GET) { response in - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "ThisIsTest2") - } - } - } - - func testCodable() async throws { - struct TestCodable: Codable { - let buffer: String - } - let (router, persist) = try createRouter() - router.put("/codable/:tag") { request, context -> HTTPResponseStatus in - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } - guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - try await persist.set(key: tag, value: TestCodable(buffer: String(buffer: buffer)), request: request) - return .ok - } - router.get("/codable/:tag") { request, context -> String? in - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } - let value = try await persist.get(key: tag, as: TestCodable.self, request: request) - return value?.buffer - } - let app = HBApplication(responder: router.buildResponder()) - - try await app.test(.router) { client in - - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/codable/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "Persist")) { _ in } - try await client.XCTExecute(uri: "/codable/\(tag)", method: .GET) { response in - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "Persist") - } - } - } - - func testRemove() async throws { - let (router, _) = try createRouter() - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/persist/\(tag)", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest1")) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .DELETE) { _ in } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - XCTAssertEqual(response.status, .noContent) - } - } - } - - func testExpireAndAdd() async throws { - let (router, _) = try createRouter() - let app = HBApplication(responder: router.buildResponder()) - try await app.test(.router) { client in - - let tag = UUID().uuidString - try await client.XCTExecute(uri: "/persist/\(tag)/0", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest1")) { _ in } - try await Task.sleep(nanoseconds: 1_000_000_000) - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - XCTAssertEqual(response.status, .noContent) - } - try await client.XCTExecute(uri: "/persist/\(tag)/10", method: .PUT, body: ByteBufferAllocator().buffer(string: "ThisIsTest1")) { response in - XCTAssertEqual(response.status, .ok) - } - try await client.XCTExecute(uri: "/persist/\(tag)", method: .GET) { response in - XCTAssertEqual(response.status, .ok) - let body = try XCTUnwrap(response.body) - XCTAssertEqual(String(buffer: body), "ThisIsTest1") - } - } - } -} diff --git a/Tests/HummingbirdTests/PersistTests.swift b/Tests/HummingbirdTests/PersistTests.swift index 81c0afcf7..cba6fc152 100644 --- a/Tests/HummingbirdTests/PersistTests.swift +++ b/Tests/HummingbirdTests/PersistTests.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -@testable import Hummingbird +import Hummingbird import HummingbirdXCT import XCTest @@ -26,14 +26,15 @@ final class PersistTests: XCTestCase { router.put("/persist/:tag") { request, context -> HTTPResponseStatus in guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } + let tag = try context.parameters.require("tag") try await persist.set(key: tag, value: String(buffer: buffer), request: request) return .ok } router.put("/persist/:tag/:time") { request, context -> HTTPResponseStatus in guard let time = context.parameters.get("time", as: Int.self) else { throw HBHTTPError(.badRequest) } - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - try await persist.set(key: tag, value: String(buffer: buffer), expires: .seconds(numericCast(time)), request: request).get() + let tag = try context.parameters.require("tag") + try await persist.set(key: tag, value: String(buffer: buffer), expires: .seconds(numericCast(time)), request: request) return .ok } router.get("/persist/:tag") { request, context -> String? in @@ -67,7 +68,8 @@ final class PersistTests: XCTestCase { router.put("/create/:tag") { request, context -> HTTPResponseStatus in guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - try await persist.create(key: tag, value: String(buffer: buffer), request: request).get() + let tag = try context.parameters.require("tag") + try await persist.create(key: tag, value: String(buffer: buffer), request: request) return .ok } let app = HBApplication(responder: router.buildResponder()) @@ -86,11 +88,12 @@ final class PersistTests: XCTestCase { router.put("/create/:tag") { request, context -> HTTPResponseStatus in guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - try await persist.create(key: tag, value: String(buffer: buffer), request: request) - .flatMapErrorThrowing { error in - if let error = error as? HBPersistError, error == .duplicate { throw HBHTTPError(.conflict) } - throw error - }.get() + let tag = try context.parameters.require("tag") + do { + try await persist.create(key: tag, value: String(buffer: buffer), request: request) + } catch let error as HBPersistError where error == .duplicate { + throw HBHTTPError(.conflict) + } return .ok } let app = HBApplication(responder: router.buildResponder()) @@ -144,6 +147,10 @@ final class PersistTests: XCTestCase { } func testCodable() async throws { + #if os(macOS) + // disable macOS tests in CI. GH Actions are currently running this when they shouldn't + guard HBEnvironment().get("CI") != "true" else { throw XCTSkip() } + #endif struct TestCodable: Codable { let buffer: String } @@ -151,12 +158,13 @@ final class PersistTests: XCTestCase { router.put("/codable/:tag") { request, context -> HTTPResponseStatus in guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } - try await persist.set(key: tag, value: TestCodable(buffer: String(buffer: buffer)), request: request).get() + try await persist.set(key: tag, value: TestCodable(buffer: String(buffer: buffer)), request: request) return .ok } router.get("/codable/:tag") { request, context -> String? in guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } - return try await persist.get(key: tag, as: TestCodable.self, request: request).get()?.buffer + let value = try await persist.get(key: tag, as: TestCodable.self, request: request) + return value?.buffer } let app = HBApplication(responder: router.buildResponder()) diff --git a/Tests/HummingbirdTests/TracingTests.swift b/Tests/HummingbirdTests/TracingTests.swift index 2e5e12043..2e5e9cd30 100644 --- a/Tests/HummingbirdTests/TracingTests.swift +++ b/Tests/HummingbirdTests/TracingTests.swift @@ -386,7 +386,8 @@ final class TracingTests: XCTestCase { router.middlewares.add(SpanMiddleware()) router.middlewares.add(HBTracingMiddleware()) router.get("/") { _, context -> HTTPResponseStatus in - return try await context.eventLoop.scheduleTask(in: .milliseconds(2)) { return .ok }.futureResult.get() + return .ok + try await Task.sleep(for: .milliseconds(2)) } let app = HBApplication(responder: router.buildResponder()) try await app.test(.router) { client in From 7eb1f8cefa00ef556ab88f098e05fcae6ef5bfa2 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 10:02:39 +0100 Subject: [PATCH 5/8] Update some more docs --- documentation/Encoding and Decoding.md | 5 ++--- documentation/Router.md | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/documentation/Encoding and Decoding.md b/documentation/Encoding and Decoding.md index cc13fd146..22f739886 100644 --- a/documentation/Encoding and Decoding.md +++ b/documentation/Encoding and Decoding.md @@ -46,14 +46,13 @@ struct User: Decodable { let firstName: String let surname: String } -app.router.post("user") { request -> EventLoopFuture in +app.router.post("user") { request async throws -> HTTPResponseStatus in // decode user from request guard let user = try? request.decode(as: User.self) else { throw HBHTTPError(.badRequest) } // create user and if ok return `.ok` status - return createUser(user, on: context.eventLoop) - .map { _ in .ok } + return try await createUser(user) } ``` Like the standard `Decoder.decode` functions `HBRequest.decode` can throw an error if decoding fails. In this situation when I received a decode error I return a failed `EventLoopFuture`. I use the function `HBcontext.failure` to generate the failed `EventLoopFuture`. diff --git a/documentation/Router.md b/documentation/Router.md index 04d1e2a97..e2a9b4a01 100644 --- a/documentation/Router.md +++ b/documentation/Router.md @@ -88,10 +88,10 @@ struct AddOrder: HBRouteHandler { self.input = try request.decode(as: Input.self) self.user = try request.auth.require(User.self) } - func handle(request: HBRequest) -> EventLoopFuture { + func handle(request: HBRequest) async throws -> Output { let order = Order(user: self.user.id, details: self.input) - return order.save(on: request.db) - .map { .init(id: order.id) } + try await order.save(on: request.db) + return Output(id: order.id) } } ``` From 65d02d6b6e0d9680b12a59488895228bd926578a Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 10:04:03 +0100 Subject: [PATCH 6/8] Update accompanying text --- documentation/Encoding and Decoding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Encoding and Decoding.md b/documentation/Encoding and Decoding.md index 22f739886..aff71818f 100644 --- a/documentation/Encoding and Decoding.md +++ b/documentation/Encoding and Decoding.md @@ -55,7 +55,7 @@ app.router.post("user") { request async throws -> HTTPResponseStatus in return try await createUser(user) } ``` -Like the standard `Decoder.decode` functions `HBRequest.decode` can throw an error if decoding fails. In this situation when I received a decode error I return a failed `EventLoopFuture`. I use the function `HBcontext.failure` to generate the failed `EventLoopFuture`. +Like the standard `Decoder.decode` functions `HBRequest.decode` can throw an error if decoding fails. In this situation when I received a decode error I throw a bad request error. I HBHTTPError to ensure that the error gets converted to an HTTP response with that status code. ## Encoding Responses From fc14609bd96e6486c5a41ed73ff8e3123934e904 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 10:37:15 +0100 Subject: [PATCH 7/8] Fix colliding test name --- Tests/HummingbirdFoundationTests/FilesTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/HummingbirdFoundationTests/FilesTests.swift b/Tests/HummingbirdFoundationTests/FilesTests.swift index 3ff65317d..55ace3ae2 100644 --- a/Tests/HummingbirdFoundationTests/FilesTests.swift +++ b/Tests/HummingbirdFoundationTests/FilesTests.swift @@ -53,7 +53,7 @@ class HummingbirdFilesTests: XCTestCase { } } - func testRead() async throws { + func testReadFileIO() async throws { let app = HBApplicationBuilder(requestContext: HBTestRouterContext.self) app.router.get("test.jpg") { _, context -> HBResponse in let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) From 0994b396dcc825546f956ca1d97650db71225cc7 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Tue, 31 Oct 2023 12:17:56 +0100 Subject: [PATCH 8/8] Update Rebase --- Tests/HummingbirdFoundationTests/FilesTests.swift | 8 +++++--- Tests/HummingbirdTests/PersistTests.swift | 3 --- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Tests/HummingbirdFoundationTests/FilesTests.swift b/Tests/HummingbirdFoundationTests/FilesTests.swift index 55ace3ae2..dbfd9ad8f 100644 --- a/Tests/HummingbirdFoundationTests/FilesTests.swift +++ b/Tests/HummingbirdFoundationTests/FilesTests.swift @@ -54,8 +54,8 @@ class HummingbirdFilesTests: XCTestCase { } func testReadFileIO() async throws { - let app = HBApplicationBuilder(requestContext: HBTestRouterContext.self) - app.router.get("test.jpg") { _, context -> HBResponse in + let router = HBRouterBuilder(context: HBTestRouterContext.self) + router.get("test.jpg") { _, context -> HBResponse in let fileIO = HBFileIO(threadPool: context.applicationContext.threadPool) let body = try await fileIO.loadFile(path: "test.jpg", context: context, logger: context.logger) return .init(status: .ok, headers: [:], body: body) @@ -66,7 +66,9 @@ class HummingbirdFilesTests: XCTestCase { XCTAssertNoThrow(try data.write(to: fileURL)) defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } - try await app.buildAndTest(.router) { client in + let app = HBApplication(responder: router.buildResponder()) + + try await app.test(.router) { client in try await client.XCTExecute(uri: "/test.jpg", method: .GET) { response in XCTAssertEqual(response.body, buffer) } diff --git a/Tests/HummingbirdTests/PersistTests.swift b/Tests/HummingbirdTests/PersistTests.swift index cba6fc152..2b5b0bdcd 100644 --- a/Tests/HummingbirdTests/PersistTests.swift +++ b/Tests/HummingbirdTests/PersistTests.swift @@ -24,7 +24,6 @@ final class PersistTests: XCTestCase { let persist = HBMemoryPersistDriver() router.put("/persist/:tag") { request, context -> HTTPResponseStatus in - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } let tag = try context.parameters.require("tag") try await persist.set(key: tag, value: String(buffer: buffer), request: request) @@ -66,7 +65,6 @@ final class PersistTests: XCTestCase { let (router, persist) = try createRouter() router.put("/create/:tag") { request, context -> HTTPResponseStatus in - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } let tag = try context.parameters.require("tag") try await persist.create(key: tag, value: String(buffer: buffer), request: request) @@ -86,7 +84,6 @@ final class PersistTests: XCTestCase { func testDoubleCreateFail() async throws { let (router, persist) = try createRouter() router.put("/create/:tag") { request, context -> HTTPResponseStatus in - guard let tag = context.parameters.get("tag") else { throw HBHTTPError(.badRequest) } guard let buffer = request.body.buffer else { throw HBHTTPError(.badRequest) } let tag = try context.parameters.require("tag") do {