diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index e3549dc..c96a10b 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -22,12 +22,7 @@ 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), @@ -50,6 +45,7 @@ struct Example { ) ) ) + 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) @@ -60,13 +56,7 @@ struct Example { // MARK: - Server Extensions @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -extension NIOHTTPServer -where RequestHandler == HTTPServerClosureRequestHandler< - HTTPRequestConcludingAsyncReader, - HTTPRequestConcludingAsyncReader.Underlying, - HTTPResponseConcludingAsyncWriter, - HTTPResponseConcludingAsyncWriter.Underlying -> { +extension NIOHTTPServer { /// Serve HTTP requests using a middleware chain built with the provided builder /// This method handles the type inference for HTTP middleware components func serve( diff --git a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift index f303ae9..24dba62 100644 --- a/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerClosureRequestHandler.swift @@ -25,9 +25,9 @@ public import HTTPTypes /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) public struct HTTPServerClosureRequestHandler< - ConcludingRequestReader: ConcludingAsyncReader & ~Copyable, + ConcludingRequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype, RequestReader: AsyncReader, any Error> & ~Copyable, - ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable, + ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype, RequestWriter: AsyncWriter, any Error> & ~Copyable >: HTTPServerRequestHandler { /// The underlying closure that handles HTTP requests @@ -35,8 +35,8 @@ public struct HTTPServerClosureRequestHandler< nonisolated(nonsending) @Sendable ( HTTPRequest, HTTPRequestContext, - consuming sending HTTPRequestConcludingAsyncReader, - consuming sending HTTPResponseSender + consuming sending ConcludingRequestReader, + consuming sending HTTPResponseSender ) async throws -> Void /// Creates a new closure-based HTTP request handler. @@ -47,8 +47,8 @@ public struct HTTPServerClosureRequestHandler< handler: nonisolated(nonsending) @Sendable @escaping ( HTTPRequest, HTTPRequestContext, - consuming sending HTTPRequestConcludingAsyncReader, - consuming sending HTTPResponseSender + consuming sending ConcludingRequestReader, + consuming sending HTTPResponseSender ) async throws -> Void ) { self._handler = handler @@ -66,20 +66,15 @@ public struct HTTPServerClosureRequestHandler< public func handle( request: HTTPRequest, requestContext: HTTPRequestContext, - requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, - responseSender: consuming sending HTTPResponseSender + requestBodyAndTrailers: consuming sending ConcludingRequestReader, + responseSender: consuming sending HTTPResponseSender ) async throws { 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 -> { +extension HTTPServerProtocol { /// 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. @@ -108,8 +103,8 @@ extension HTTPServerProtocol where RequestHandler == HTTPServerClosureRequestHan handler: @Sendable @escaping ( _ request: HTTPRequest, _ requestContext: HTTPRequestContext, - _ requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, - _ responseSender: consuming sending HTTPResponseSender + _ requestBodyAndTrailers: consuming sending RequestReader, + _ 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 3e4eadc..4837f95 100644 --- a/Sources/HTTPServer/HTTPServerProtocol.swift +++ b/Sources/HTTPServer/HTTPServerProtocol.swift @@ -3,11 +3,21 @@ 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 we can't make the serve method generic over the handler - // because otherwise, closure-based APIs can't be implemented. + /// The ``ConcludingAsyncReader`` to use when reading requests. ``ConcludingAsyncReader/FinalElement`` + /// must be an optional `HTTPFields`, and ``ConcludingAsyncReader/Underlying`` must use `Span` as its + /// `ReadElement`. + associatedtype RequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype + where RequestReader.Underlying.ReadElement == Span, + RequestReader.Underlying.ReadFailure == any Error, + RequestReader.FinalElement == HTTPFields? - /// The ``HTTPServerRequestHandler`` to use when handling requests. - associatedtype RequestHandler: HTTPServerRequestHandler + /// The ``ConcludingAsyncWriter`` to use when writing responses. ``ConcludingAsyncWriter/FinalElement`` + /// must be an optional `HTTPFields`, and ``ConcludingAsyncWriter/Underlying`` must use `Span` as its + /// `WriteElement`. + associatedtype ResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype + where ResponseWriter.Underlying.WriteElement == Span, + ResponseWriter.Underlying.WriteFailure == any Error, + ResponseWriter.FinalElement == HTTPFields? /// Starts an HTTP server with the specified request handler. /// @@ -18,7 +28,7 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { /// /// - Parameters: /// - handler: A ``HTTPServerRequestHandler`` implementation that processes incoming HTTP requests. The handler - /// receives each request along with a body reader and ``HTTPResponseSender``. + /// receives each request along with its context, a body and trailers reader, and an ``HTTPResponseSender``. /// /// ## Example /// @@ -26,8 +36,9 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { /// struct EchoHandler: HTTPServerRequestHandler { /// func handle( /// request: HTTPRequest, - /// requestBodyAndTrailers: consuming HTTPRequestConcludingAsyncReader, - /// responseSender: consuming HTTPResponseSender + /// requestContext: HTTPRequestContext, + /// requestBodyAndTrailers: consuming sending HTTPRequestConcludingAsyncReader, + /// responseSender: consuming sending HTTPResponseSender /// ) async throws { /// let response = HTTPResponse(status: .ok) /// let writer = try await responseSender.send(response) @@ -39,5 +50,5 @@ public protocol HTTPServerProtocol: Sendable, ~Copyable, ~Escapable { /// /// try await server.serve(handler: EchoHandler()) /// ``` - func serve(handler: RequestHandler) async throws + func serve(handler: some HTTPServerRequestHandler) async throws } diff --git a/Sources/HTTPServer/HTTPServerRequestHandler.swift b/Sources/HTTPServer/HTTPServerRequestHandler.swift index 4cdbe68..2f920d3 100644 --- a/Sources/HTTPServer/HTTPServerRequestHandler.swift +++ b/Sources/HTTPServer/HTTPServerRequestHandler.swift @@ -3,11 +3,11 @@ public import HTTPTypes /// A protocol that defines the contract for handling HTTP server requests. /// /// ``HTTPServerRequestHandler`` provides a structured way to process incoming HTTP requests and generate appropriate responses. -/// Conforming types implement the ``handle(request:requestBodyAndTrailers:responseSender:)`` method, +/// Conforming types implement the ``handle(request:requestContext:requestBodyAndTrailers:responseSender:)`` method, /// which is called by the HTTP server for each incoming request. The handler is responsible for: /// /// - Processing the request headers. -/// - Reading the request body data using the provided ``HTTPRequestConcludingAsyncReader`` +/// - Reading the request body data using the provided `RequestReader` /// - Generating and sending an appropriate response using the response callback /// /// This protocol fully supports bi-directional streaming HTTP request handling including the optional request and response trailers. @@ -57,34 +57,30 @@ public import HTTPTypes /// } /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -public protocol HTTPServerRequestHandler: Sendable { +public protocol HTTPServerRequestHandler: Sendable { /// The ``ConcludingAsyncReader`` to use when reading requests. ``ConcludingAsyncReader/FinalElement`` /// must be an optional `HTTPFields`, and ``ConcludingAsyncReader/Underlying`` must use `Span` as its /// `ReadElement`. - associatedtype ConcludingRequestReader: ConcludingAsyncReader & ~Copyable + associatedtype RequestReader: ConcludingAsyncReader & ~Copyable & SendableMetatype + where RequestReader.Underlying.ReadElement == Span, + RequestReader.FinalElement == HTTPFields? - /// The underlying ``AsyncReader`` for ``ConcludingRequestReader``. Its ``AsyncReader/ReadElement`` must - /// be `Span`. - associatedtype RequestReader: AsyncReader, any Error> & ~Copyable - - /// The ``ConcludingAsyncWriter`` to use when reading requests. ``ConcludingAsyncWriter/FinalElement`` + /// The ``ConcludingAsyncWriter`` to use when writing responses. ``ConcludingAsyncWriter/FinalElement`` /// must be an optional `HTTPFields`, and ``ConcludingAsyncWriter/Underlying`` must use `Span` as its /// `WriteElement`. - associatedtype ConcludingResponseWriter: ConcludingAsyncWriter & ~Copyable - - /// The underlying ``AsyncWriter`` for ``ConcludingResponseWriter``. Its ``AsyncWriter/WriteElement`` must - /// be `Span`. - associatedtype RequestWriter: AsyncWriter, any Error> & ~Copyable + associatedtype ResponseWriter: ConcludingAsyncWriter & ~Copyable & SendableMetatype + where ResponseWriter.Underlying.WriteElement == Span, + ResponseWriter.FinalElement == HTTPFields? /// Handles an incoming HTTP request and generates a response. /// /// This method is called by the HTTP server for each incoming client request. Implementations should: /// 1. Examine the request headers in the `request` parameter - /// 2. Read the request body data from the ``RequestConcludingAsyncReader`` as needed + /// 2. Read the request body data from the `RequestReader` as needed /// 3. Process the request and prepare a 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`` + /// 5. Write the response body data to the returned `ResponseWriter` /// /// - Parameters: /// - request: The HTTP request headers and metadata. @@ -99,7 +95,7 @@ public protocol HTTPServerRequestHandler: Sendable { func handle( request: HTTPRequest, requestContext: HTTPRequestContext, - requestBodyAndTrailers: consuming sending ConcludingRequestReader, - responseSender: consuming sending HTTPResponseSender + requestBodyAndTrailers: consuming sending RequestReader, + responseSender: consuming sending HTTPResponseSender ) async throws } diff --git a/Sources/HTTPServer/NIOHTTPServer.swift b/Sources/HTTPServer/NIOHTTPServer.swift index c5364d4..71bfa57 100644 --- a/Sources/HTTPServer/NIOHTTPServer.swift +++ b/Sources/HTTPServer/NIOHTTPServer.swift @@ -62,9 +62,10 @@ import Synchronization /// } /// ``` @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) -public struct NIOHTTPServer: HTTPServerProtocol -where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader, - RequestHandler.ConcludingResponseWriter == HTTPResponseConcludingAsyncWriter { +public struct NIOHTTPServer: HTTPServerProtocol { + public typealias RequestReader = HTTPRequestConcludingAsyncReader + public typealias ResponseWriter = HTTPResponseConcludingAsyncWriter + private let logger: Logger private let configuration: HTTPServerConfiguration @@ -118,7 +119,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader /// handler: EchoHandler() /// ) /// ``` - public func serve(handler: RequestHandler) async throws { + public func serve(handler: some HTTPServerRequestHandler) async throws { let asyncChannelConfiguration: NIOAsyncChannel.Configuration switch self.configuration.backpressureStrategy.backing { case .watermark(let low, let high): @@ -274,7 +275,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader private func serveInsecureHTTP1_1( bindTarget: HTTPServerConfiguration.BindTarget, - handler: RequestHandler, + handler: some HTTPServerRequestHandler, asyncChannelConfiguration: NIOAsyncChannel.Configuration ) async throws { switch bindTarget.backing { @@ -309,7 +310,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader private func serveSecureUpgrade( bindTarget: HTTPServerConfiguration.BindTarget, tlsConfiguration: TLSConfiguration, - handler: RequestHandler, + handler: some HTTPServerRequestHandler, asyncChannelConfiguration: NIOAsyncChannel.Configuration, http2Configuration: NIOHTTP2Handler.Configuration ) async throws { @@ -394,7 +395,7 @@ where RequestHandler.ConcludingRequestReader == HTTPRequestConcludingAsyncReader private func handleRequestChannel( channel: NIOAsyncChannel, - handler: RequestHandler + handler: some HTTPServerRequestHandler ) async throws { do { try await channel diff --git a/Tests/HTTPServerTests/HTTPServerTests.swift b/Tests/HTTPServerTests/HTTPServerTests.swift index 21501eb..edca07b 100644 --- a/Tests/HTTPServerTests/HTTPServerTests.swift +++ b/Tests/HTTPServerTests/HTTPServerTests.swift @@ -8,12 +8,7 @@ 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)) )