diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index de8bb77..e3549dc 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -22,7 +22,12 @@ struct Example { // Using the new extension method that doesn't require type hints let privateKey = P256.Signing.PrivateKey() - let server = NIOHTTPServer>( + let server = NIOHTTPServer>( logger: logger, configuration: .init( bindTarget: .hostAndPort(host: "127.0.0.1", port: 12345), @@ -45,7 +50,7 @@ struct Example { ) ) ) - try await server.serve { request, requestBodyAndTrailers, responseSender in + try await server.serve { request, requestContext, requestBodyAndTrailers, responseSender in let writer = try await responseSender.send(HTTPResponse(status: .ok)) try await writer.writeAndConclude(element: "Well, hello!".utf8.span, finalElement: nil) } @@ -54,16 +59,20 @@ struct Example { // MARK: - Server Extensions -// This has to be commented out because of the compiler bug above. Workaround doesn't apply here. - @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler { +extension NIOHTTPServer +where RequestHandler == HTTPServerClosureRequestHandler< + HTTPRequestConcludingAsyncReader, + HTTPRequestConcludingAsyncReader.Underlying, + HTTPResponseConcludingAsyncWriter, + HTTPResponseConcludingAsyncWriter.Underlying +> { /// Serve HTTP requests using a middleware chain built with the provided builder /// This method handles the type inference for HTTP middleware components func serve( @MiddlewareChainBuilder withMiddleware middlewareBuilder: () -> some Middleware< - RequestResponseMiddlewareBox< + RequestResponseMiddlewareBox< HTTPRequestConcludingAsyncReader, HTTPResponseConcludingAsyncWriter >, @@ -72,9 +81,10 @@ extension NIOHTTPServer where RequestHandler == HTTPServerClosureRequestHandler< ) async throws { let chain = middlewareBuilder() - try await self.serve { request, reader, responseSender in + try await self.serve { request, requestContext, reader, responseSender in try await chain.intercept(input: RequestResponseMiddlewareBox( request: request, + requestContext: requestContext, requestReader: reader, responseSender: responseSender )) { _ in } diff --git a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift index 9617d92..d90ae4b 100644 --- a/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift +++ b/Sources/Example/Middlewares/HTTPRequestLoggingMiddleware.swift @@ -34,7 +34,7 @@ where input: consuming Input, next: (consuming NextInput) async throws -> Void ) async throws { - try await input.withContents { request, requestReader, responseSender in + try await input.withContents { request, context, requestReader, responseSender in self.logger.info("Received request \(request.path ?? "unknown" ) \(request.method.rawValue)") defer { self.logger.info("Finished request \(request.path ?? "unknown" ) \(request.method.rawValue)") @@ -47,6 +47,7 @@ where var maybeSender = Optional(responseSender) let requestResponseBox = RequestResponseMiddlewareBox( request: request, + requestContext: context, requestReader: wrappedReader, responseSender: HTTPResponseSender { [logger] response in if let sender = maybeSender.take() { diff --git a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift index d4b6494..a6a62bb 100644 --- a/Sources/Example/Middlewares/RouteHandlerMiddleware.swift +++ b/Sources/Example/Middlewares/RouteHandlerMiddleware.swift @@ -20,7 +20,7 @@ where input: consuming Input, next: (consuming NextInput) async throws -> Void ) async throws { - try await input.withContents { request, requestReader, responseSender in + try await input.withContents { request, _, requestReader, responseSender in var maybeReader = Optional(requestReader) try await responseSender.send(HTTPResponse(status: .accepted)) .produceAndConclude { responseBodyAsyncWriter in diff --git a/Sources/HTTPServer/HTTPRequestContext.swift b/Sources/HTTPServer/HTTPRequestContext.swift new file mode 100644 index 0000000..9df1ea5 --- /dev/null +++ b/Sources/HTTPServer/HTTPRequestContext.swift @@ -0,0 +1,4 @@ +/// A context object that carries additional information about an HTTP request. +/// +/// `HTTPRequestContext` provides a way to pass metadata through the HTTP request pipeline. +public struct HTTPRequestContext: Sendable {} diff --git a/Sources/HTTPServer/HTTPResponseSender.swift b/Sources/HTTPServer/HTTPResponseSender.swift index eafa4a4..fe56c02 100644 --- a/Sources/HTTPServer/HTTPResponseSender.swift +++ b/Sources/HTTPServer/HTTPResponseSender.swift @@ -3,7 +3,7 @@ public import HTTPTypes /// This type ensures that a single non-informational (1xx) `HTTPResponse` is sent back to the client when handling a request. /// /// The user will get a ``HTTPResponseSender`` as part of -/// ``HTTPServerRequestHandler/handle(request:requestBodyAndTrailers:responseSender:)``, and they +/// ``HTTPServerRequestHandler/handle(request:requestContext:requestBodyAndTrailers:responseSender:)``, and they /// will only be allowed to call ``send(_:)`` once before the sender is consumed and cannot be referenced again. /// ``sendInformational(_:)`` may be called zero or more times. /// diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index 461096f..f303ae9 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -8,7 +8,7 @@ public import HTTPTypes /// /// - Example: /// ```swift -/// let echoHandler = HTTPServerClosureRequestHandler { request, bodyReader, sendResponse in +/// let echoHandler = HTTPServerClosureRequestHandler { request, context, bodyReader, responseSender in /// // Read the entire request body /// let (bodyData, _) = try await bodyReader.consumeAndConclude { reader in /// // ... body reading code ... @@ -16,7 +16,7 @@ public import HTTPTypes /// /// // Create and send response /// var response = HTTPResponse(status: .ok) -/// let responseWriter = try await sendResponse(response) +/// let responseWriter = try await responseSender.send(response) /// try await responseWriter.produceAndConclude { writer in /// try await writer.write(bodyData.span) /// return ((), nil) @@ -24,11 +24,17 @@ public import HTTPTypes /// } /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -public struct HTTPServerClosureRequestHandler: HTTPServerRequestHandler { +public struct HTTPServerClosureRequestHandler< + ConcludingRequestReader: ConcludingAsyncReader & ~Copyable, + RequestReader: AsyncReader, any Error> & ~Copyable, + ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable, + RequestWriter: AsyncWriter, any Error> & ~Copyable +>: HTTPServerRequestHandler { /// The underlying closure that handles HTTP requests private let _handler: nonisolated(nonsending) @Sendable ( HTTPRequest, + HTTPRequestContext, consuming sending HTTPRequestConcludingAsyncReader, consuming sending HTTPResponseSender ) async throws -> Void @@ -40,6 +46,7 @@ public struct HTTPServerClosureRequestHandler ) async throws -> Void @@ -53,13 +60,58 @@ public struct HTTPServerClosureRequestHandler ) async throws { - try await self._handler(request, requestBodyAndTrailers, responseSender) + try await self._handler(request, requestContext, requestBodyAndTrailers, responseSender) + } +} + +@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) +extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler< + HTTPRequestConcludingAsyncReader, + HTTPRequestConcludingAsyncReader.Underlying, + HTTPResponseConcludingAsyncWriter, + HTTPResponseConcludingAsyncWriter.Underlying +> { + /// Starts an HTTP server with a closure-based request handler. + /// + /// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests. + /// + /// - Parameters: + /// - handler: An async closure that processes HTTP requests. The closure receives: + /// - `HTTPRequest`: The incoming HTTP request with headers and metadata. + /// - ``HTTPRequestContext``: The request's context. + /// - ``HTTPRequestConcludingAsyncReader``: An async reader for consuming the request body and trailers. + /// - ``HTTPResponseSender``: A non-copyable wrapper for a function that accepts an `HTTPResponse` and provides access to an ``HTTPResponseConcludingAsyncWriter``. + /// + /// ## Example + /// + /// ```swift + /// try await server.serve { request, bodyReader, responseSender in + /// // Process the request + /// let response = HTTPResponse(status: .ok) + /// let writer = try await responseSender.send(response) + /// try await writer.produceAndConclude { writer in + /// try await writer.write("Hello, World!".utf8) + /// return ((), nil) + /// } + /// } + /// ``` + public func serve( + handler: @Sendable @escaping ( + _ request: HTTPRequest, + _ requestContext: HTTPRequestContext, + _ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, + _ responseSender: consuming sending HTTPResponseSender + ) async throws -> Void + ) async throws { + try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler)) } } diff --git a/Sources/HTTPServer/HTTPServerProtocol.swift b/Sources/HTTPServer/HTTPServerProtocol.swift index d93f1d5..3e4eadc 100644 --- a/Sources/HTTPServer/HTTPServerProtocol.swift +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -3,8 +3,8 @@ public import HTTPTypes @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) /// A generic HTTP server protocol that can handle incoming HTTP requests. public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { - // TODO: write down in the proposal why we can't make the serve method generic over the handler (closure-based APIs can't - // be implemented) + // TODO: write down in the proposal we can't make the serve method generic over the handler + // because otherwise, closure-based APIs can't be implemented. /// The ``HTTPServerRequestHandler`` to use when handling requests. associatedtype RequestHandler: HTTPServerRequestHandler @@ -41,39 +41,3 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { /// ``` func serve(handler: RequestHandler) async throws } - -@available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHandler { - /// Starts an HTTP server with a closure-based request handler. - /// - /// This method provides a convenient way to start an HTTP server using a closure to handle incoming requests. - /// - /// - Parameters: - /// - handler: An async closure that processes HTTP requests. The closure receives: - /// - `HTTPRequest`: The incoming HTTP request with headers and metadata - /// - ``HTTPRequestConcludingAsyncReader``: An async reader for consuming the request body and trailers - /// - ``HTTPResponseSender``: A non-copyable wrapper for a function that accepts an `HTTPResponse` and provides access to an ``HTTPResponseConcludingAsyncWriter`` - /// - /// ## Example - /// - /// ```swift - /// try await server.serve { request, bodyReader, sendResponse in - /// // Process the request - /// let response = HTTPResponse(status: .ok) - /// let writer = try await sendResponse(response) - /// try await writer.produceAndConclude { writer in - /// try await writer.write("Hello, World!".utf8) - /// return ((), nil) - /// } - /// } - /// ``` - public func serve( - handler: nonisolated(nonsending) @Sendable @escaping ( - _ request: HTTPRequest, - _ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, - _ responseSender: consuming sending HTTPResponseSender - ) async throws -> Void - ) async throws { - try await self.serve(handler: HTTPServerClosureRequestHandler(handler: handler)) - } -} diff --git a/Sources/HTTPServer/HTTPServerRequestHandler.swift b/Sources/HTTPServer/HTTPServerRequestHandler.swift index b209cbf..4cdbe68 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -16,11 +16,12 @@ public import HTTPTypes /// /// ```swift /// struct EchoHandler: HTTPServerRequestHandler { -/// func handle( +/// func handle( /// request: HTTPRequest, -/// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, -/// responseSender: consuming HTTPResponseSender -/// ) async throws { +/// requestContext: HTTPRequestContext, +/// requestConcludingAsyncReader: consuming sending HTTPRequestConcludingAsyncReader, +/// responseSender: consuming sending HTTPResponseSender +/// ) async throws { /// // Read the entire request body /// let (bodyData, trailers) = try await requestConcludingAsyncReader.consumeAndConclude { reader in /// var reader = reader @@ -81,12 +82,13 @@ public protocol HTTPServerRequestHandler: Sendable { /// 1. Examine the request headers in the `request` parameter /// 2. Read the request body data from the ``RequestConcludingAsyncReader`` as needed /// 3. Process the request and prepare a response - /// 4. Optionally call ``HTTPResponseSender/sendInformationalResponse(_:)`` as needed - /// 4. Call the ``HTTPResponseSender/sendResponse(_:)`` with an appropriate HTTP response + /// 4. Optionally call ``HTTPResponseSender/sendInformational(_:)`` as needed + /// 4. Call the ``HTTPResponseSender/send(_:)`` with an appropriate HTTP response /// 5. Write the response body data to the returned ``HTTPResponseConcludingAsyncWriter`` /// /// - Parameters: /// - request: The HTTP request headers and metadata. + /// - requestContext: A ``HTTPRequestContext``. /// - requestBodyAndTrailers: A reader for accessing the request body data and trailing headers. /// This follows the `ConcludingAsyncReader` pattern, allowing for incremental reading of request body data /// and concluding with any trailer fields sent at the end of the request. @@ -95,8 +97,8 @@ public protocol HTTPServerRequestHandler: Sendable { /// /// - Throws: Any error encountered during request processing or response generation. func handle( - // TODO: add request context parameter request: HTTPRequest, + requestContext: HTTPRequestContext, requestBodyAndTrailers: consuming sending ConcludingRequestReader, responseSender: consuming sending HTTPResponseSender ) async throws diff --git a/Sources/HTTPServer/NIOHTTPServer.swift b/Sources/HTTPServer/NIOHTTPServer.swift index 566619a..c5364d4 100644 --- a/Sources/HTTPServer/NIOHTTPServer.swift +++ b/Sources/HTTPServer/NIOHTTPServer.swift @@ -424,6 +424,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader do { try await handler.handle( request: httpRequest, + requestContext: HTTPRequestContext(), requestBodyAndTrailers: HTTPRequestConcludingAsyncReader( iterator: iterator, readerState: readerState diff --git a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift index 6f634af..28d6f42 100644 --- a/Sources/HTTPServer/RequestResponseMiddlewareBox.swift +++ b/Sources/HTTPServer/RequestResponseMiddlewareBox.swift @@ -8,6 +8,7 @@ public struct RequestResponseMiddlewareBox< ResponseWriter: ConcludingAsyncWriter & ~Copyable >: ~Copyable { private let request: HTTPRequest + private let requestContext: HTTPRequestContext private let requestReader: RequestReader private let responseSender: HTTPResponseSender @@ -18,10 +19,12 @@ public struct RequestResponseMiddlewareBox< /// - responseSender: The ``HTTPResponseSender``. public init( request: HTTPRequest, + requestContext: HTTPRequestContext, requestReader: consuming RequestReader, responseSender: consuming HTTPResponseSender ) { self.request = request + self.requestContext = requestContext self.requestReader = requestReader self.responseSender = responseSender } @@ -32,11 +35,17 @@ public struct RequestResponseMiddlewareBox< public consuming func withContents( _ handler: nonisolated(nonsending) ( HTTPRequest, + HTTPRequestContext, consuming RequestReader, consuming HTTPResponseSender ) async throws -> T ) async throws -> T { - try await handler(self.request, self.requestReader, self.responseSender) + try await handler( + self.request, + self.requestContext, + self.requestReader, + self.responseSender + ) } } diff --git a/Tests/HTTPServerTests/HTTPServerTests.swift b/Tests/HTTPServerTests/HTTPServerTests.swift index a1dde72..21501eb 100644 --- a/Tests/HTTPServerTests/HTTPServerTests.swift +++ b/Tests/HTTPServerTests/HTTPServerTests.swift @@ -8,11 +8,16 @@ struct HTTPServerTests { @Test @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) func testConsumingServe() async throws { - let server = NIOHTTPServer>( + let server = NIOHTTPServer>( logger: Logger(label: "Test"), configuration: .init(bindTarget: .hostAndPort(host: "127.0.0.1", port: 0)) ) - try await server.serve { request, requestBodyAndTrailers, responseSender in + try await server.serve { request, context, requestBodyAndTrailers, responseSender in _ = try await requestBodyAndTrailers.collect(upTo: 100) { _ in } // Uncommenting this would cause a "requestBodyAndTrailers consumed more than once" error. // _ = try await requestBodyAndTrailers.collect(upTo: 100) { _ in }